use crate::AppService;
use crate::error::AppError;
use chrono::Utc;
use models::repos::repo_webhook;
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;

#[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)]
pub struct WebhookEvent {
    #[serde(default)]
    pub push: bool,
    #[serde(default)]
    pub tag_push: bool,
    #[serde(default)]
    pub pull_request: bool,
    #[serde(default)]
    pub issue_comment: bool,
    #[serde(default)]
    pub release: bool,
}

impl Default for WebhookEvent {
    fn default() -> Self {
        Self {
            push: true,
            tag_push: false,
            pull_request: false,
            issue_comment: false,
            release: false,
        }
    }
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CreateWebhookParams {
    pub url: String,
    #[serde(default)]
    pub content_type: Option<String>,
    #[serde(default)]
    pub secret: Option<String>,
    #[serde(default)]
    pub insecure_ssl: Option<bool>,
    #[serde(default)]
    pub events: WebhookEvent,
    #[serde(default = "default_active")]
    pub active: bool,
}

fn default_active() -> bool {
    true
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct UpdateWebhookParams {
    pub url: Option<String>,
    pub content_type: Option<String>,
    pub secret: Option<String>,
    pub insecure_ssl: Option<bool>,
    pub events: Option<WebhookEvent>,
    pub active: Option<bool>,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct WebhookResponse {
    pub id: i64,
    pub repo_uuid: String,
    pub url: String,
    pub content_type: String,
    pub secret: Option<String>,
    pub events: WebhookEvent,
    pub active: bool,
    pub created_at: chrono::DateTime<Utc>,
    pub last_delivered_at: Option<chrono::DateTime<Utc>>,
    pub touch_count: i64,
}

impl From<repo_webhook::Model> for WebhookResponse {
    fn from(m: repo_webhook::Model) -> Self {
        let events: WebhookEvent = serde_json::from_value(m.event.clone()).unwrap_or_default();
        let url = m.url.unwrap_or_default();
        let (content_type, secret, active) =
            serde_json::from_value::<serde_json::Value>(m.event.clone())
                .ok()
                .map(|v| {
                    (
                        v.get("content_type")
                            .and_then(|v| v.as_str())
                            .unwrap_or("json")
                            .to_string(),
                        v.get("secret").and_then(|v| v.as_str()).map(String::from),
                        v.get("active").and_then(|v| v.as_bool()).unwrap_or(true),
                    )
                })
                .unwrap_or(("json".to_string(), None, true));

        WebhookResponse {
            id: m.id,
            repo_uuid: m.repo.to_string(),
            url,
            content_type,
            secret,
            events,
            active,
            created_at: m.created_at,
            last_delivered_at: m.last_delivered_at,
            touch_count: m.touch_count,
        }
    }
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct WebhookListResponse {
    pub webhooks: Vec<WebhookResponse>,
    pub total: usize,
}

impl AppService {
    pub async fn git_webhook_list(
        &self,
        namespace: String,
        repo_name: String,
        ctx: &Session,
    ) -> Result<WebhookListResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let webhooks = repo_webhook::Entity::find()
            .filter(repo_webhook::Column::Repo.eq(repo.id))
            .order_by_asc(repo_webhook::Column::Id)
            .all(&self.db)
            .await?;
        let total = webhooks.len();
        let webhooks = webhooks.into_iter().map(WebhookResponse::from).collect();
        Ok(WebhookListResponse { webhooks, total })
    }

    pub async fn git_webhook_create(
        &self,
        namespace: String,
        repo_name: String,
        params: CreateWebhookParams,
        ctx: &Session,
    ) -> Result<WebhookResponse, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let _ = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;

        let event_json = serde_json::json!({
            "push": params.events.push,
            "tag_push": params.events.tag_push,
            "pull_request": params.events.pull_request,
            "issue_comment": params.events.issue_comment,
            "release": params.events.release,
            "content_type": params.content_type.unwrap_or_else(|| "json".to_string()),
            "secret": params.secret,
            "active": params.active,
        });

        let model = repo_webhook::ActiveModel {
            repo: Set(repo.id),
            event: Set(event_json),
            url: Set(Some(params.url)),
            access_key: Set(None),
            secret_key: Set(params.secret),
            created_at: Set(Utc::now()),
            last_delivered_at: Set(None),
            touch_count: Set(0),
            ..Default::default()
        }
        .insert(&self.db)
        .await?;

        Ok(WebhookResponse::from(model))
    }

    pub async fn git_webhook_get(
        &self,
        namespace: String,
        repo_name: String,
        webhook_id: i64,
        ctx: &Session,
    ) -> Result<WebhookResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let webhook = repo_webhook::Entity::find_by_id(webhook_id)
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Webhook not found".to_string()))?;

        if webhook.repo != repo.id {
            return Err(AppError::NotFound("Webhook not found".to_string()));
        }

        Ok(WebhookResponse::from(webhook))
    }

    pub async fn git_webhook_update(
        &self,
        namespace: String,
        repo_name: String,
        webhook_id: i64,
        params: UpdateWebhookParams,
        ctx: &Session,
    ) -> Result<WebhookResponse, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let _ = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;

        let webhook = repo_webhook::Entity::find_by_id(webhook_id)
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Webhook not found".to_string()))?;

        if webhook.repo != repo.id {
            return Err(AppError::NotFound("Webhook not found".to_string()));
        }

        let mut active: repo_webhook::ActiveModel = webhook.clone().into();
        if let Some(url) = params.url {
            active.url = Set(Some(url));
        }
        let secret_val = params.secret.clone();
        if let Some(secret) = params.secret {
            active.secret_key = Set(Some(secret));
        }
        if params.events.is_some() || params.content_type.is_some() || params.active.is_some() {
            let existing: serde_json::Value = webhook.event.clone();
            let events = params
                .events
                .unwrap_or_else(|| serde_json::from_value(existing.clone()).unwrap_or_default());
            let content_type = params
                .content_type
                .or_else(|| {
                    existing
                        .get("content_type")
                        .and_then(|v| v.as_str())
                        .map(String::from)
                })
                .unwrap_or_else(|| "json".to_string());
            let active_val = params
                .active
                .or_else(|| existing.get("active").and_then(|v| v.as_bool()))
                .unwrap_or(true);
            let secret_in_event = existing
                .get("secret")
                .and_then(|v| v.as_str())
                .map(String::from);
            let final_secret = secret_val.clone().or(secret_in_event);

            active.event = Set(serde_json::json!({
                "push": events.push,
                "tag_push": events.tag_push,
                "pull_request": events.pull_request,
                "issue_comment": events.issue_comment,
                "release": events.release,
                "content_type": content_type,
                "secret": final_secret,
                "active": active_val,
            }));
        }

        let updated = active.update(&self.db).await?;
        Ok(WebhookResponse::from(updated))
    }

    pub async fn git_webhook_delete(
        &self,
        namespace: String,
        repo_name: String,
        webhook_id: i64,
        ctx: &Session,
    ) -> Result<(), AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let _ = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;

        let webhook = repo_webhook::Entity::find_by_id(webhook_id)
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Webhook not found".to_string()))?;

        if webhook.repo != repo.id {
            return Err(AppError::NotFound("Webhook not found".to_string()));
        }

        repo_webhook::Entity::delete_by_id(webhook_id)
            .exec(&self.db)
            .await?;
        Ok(())
    }
}
