use db::{sqlx, sqlx::AssertSqlSafe};
use model::{issues::IssueModel, users::UserModel};
use serde::Deserialize;
use session::Session;

use super::types::{IssueFilter, IssueResponse, issue_author};
use crate::{AppService, Pagination, error::AppError, session_user};

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CreateIssue {
    pub title: String,
    pub body: Option<String>,
    pub priority: Option<String>,
    pub due_at: Option<String>,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct UpdateIssue {
    pub title: Option<String>,
    pub body: Option<String>,
    pub priority: Option<String>,
    pub due_at: Option<String>,
}

impl AppService {
    pub async fn issue_create(
        &self,
        ctx: &Session,
        wk_name: &str,
        params: CreateIssue,
    ) -> Result<IssueResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(wk_name).await?;
        self.workspace_require_member(wk.id, user_uid).await?;

        let title = params.title.trim();
        if title.is_empty() {
            return Err(AppError::BadRequest(
                "issue title is required".to_string(),
            ));
        }

        let now = chrono::Utc::now();
        let id = uuid::Uuid::now_v7();
        let priority = params.priority.unwrap_or_else(|| "normal".to_string());
        let due_at = params.due_at.and_then(|d| {
            chrono::DateTime::parse_from_rfc3339(&d)
                .ok()
                .map(|dt| dt.to_utc())
        });

        let mut txn = self.db.begin().await.map_err(|_| AppError::TxnError)?;

        let issue = sqlx::query_as::<_, IssueModel>(
            "INSERT INTO issue (id, wk, number, title, body, state, priority, author, due_at, created_at, updated_at) \
             VALUES ($1, $2, (SELECT COALESCE(MAX(number), 0) + 1 FROM issue WHERE wk = $2 AND deleted_at IS NULL), \
             $3, $4, 'open', $5, $6, $7, $8, $8) \
             RETURNING id, wk, number, title, body, state, priority, author, closed_by, closed_at, due_at, created_at, updated_at, deleted_at",
        )
        .bind(id)
        .bind(wk.id)
        .bind(title)
        .bind(&params.body)
        .bind(&priority)
        .bind(user_uid)
        .bind(due_at)
        .bind(now)
        .fetch_one(&mut **txn.inner_mut())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        sqlx::query(
            "INSERT INTO issue_event (id, issue, actor, event, from_value, to_value, created_at) \
             VALUES ($1, $2, $3, 'created', NULL, $4, $5)",
        )
        .bind(uuid::Uuid::now_v7())
        .bind(issue.id)
        .bind(user_uid)
        .bind(&issue.title)
        .bind(now)
        .execute(&mut **txn.inner_mut())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        txn.commit().await.map_err(|_| AppError::TxnError)?;

        let author = self.users_find_by_id(user_uid).await?;
        Ok(IssueResponse {
            number: issue.number,
            title: issue.title,
            body: issue.body,
            state: issue.state,
            priority: issue.priority,
            due_at: issue.due_at,
            author: issue_author(author),
            closed_by: None,
            closed_at: None,
            created_at: issue.created_at,
            updated_at: issue.updated_at,
            labels: Vec::new(),
            assignees: Vec::new(),
            milestone: None,
            repos: Vec::new(),
            pull_requests: Vec::new(),
        })
    }

    pub async fn issue_list(
        &self,
        ctx: &Session,
        wk_name: &str,
        filter: IssueFilter,
        pagination: Pagination,
    ) -> Result<Vec<IssueResponse>, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(wk_name).await?;
        self.workspace_require_member(wk.id, user_uid).await?;

        let mut conditions =
            vec!["i.wk = $1".to_string(), "i.deleted_at IS NULL".to_string()];
        let mut param_idx = 2;

        if filter.state.is_some() {
            conditions.push(format!("i.state = ${param_idx}"));
            param_idx += 1;
        }
        if filter.priority.is_some() {
            conditions.push(format!("i.priority = ${param_idx}"));
            param_idx += 1;
        }
        if filter.label.is_some() {
            conditions.push(format!(
                "EXISTS(SELECT 1 FROM issue_label il INNER JOIN label l ON l.id = il.label WHERE il.issue = i.id AND l.name = ${param_idx})"
            ));
            param_idx += 1;
        }
        if filter.milestone.is_some() {
            conditions.push(format!(
                "EXISTS(SELECT 1 FROM issue_milestone im INNER JOIN milestone m ON m.id = im.milestone WHERE im.issue = i.id AND m.title = ${param_idx})"
            ));
            param_idx += 1;
        }
        if filter.assignee.is_some() {
            conditions.push(format!(
                "EXISTS(SELECT 1 FROM issue_assignee ia INNER JOIN \"user\" u ON u.id = ia.\"user\" WHERE ia.issue = i.id AND u.username = ${param_idx})"
            ));
            param_idx += 1;
        }

        let where_clause = conditions.join(" AND ");
        let limit_idx = param_idx;
        let offset_idx = param_idx + 1;

        let query = format!(
            "SELECT i.id, i.wk, i.number, i.title, i.body, i.state, i.priority, i.author, \
             i.closed_by, i.closed_at, i.due_at, i.created_at, i.updated_at, i.deleted_at \
             FROM issue i WHERE {where_clause} \
             ORDER BY i.created_at DESC LIMIT ${limit_idx} OFFSET ${offset_idx}"
        );

        let mut q =
            sqlx::query_as::<_, IssueModel>(AssertSqlSafe(query)).bind(wk.id);
        if let Some(state) = &filter.state {
            q = q.bind(state);
        }
        if let Some(priority) = &filter.priority {
            q = q.bind(priority);
        }
        if let Some(label_name) = &filter.label {
            q = q.bind(label_name);
        }
        if let Some(milestone_title) = &filter.milestone {
            q = q.bind(milestone_title);
        }
        if let Some(assignee_username) = &filter.assignee {
            q = q.bind(assignee_username);
        }
        q = q
            .bind(pagination.limit() as i64)
            .bind(pagination.offset() as i64);

        let issues = q
            .fetch_all(self.db.reader())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        let mut results = Vec::new();
        for issue in issues {
            results.push(self.issue_build_response(issue).await?);
        }
        Ok(results)
    }

    pub async fn issue_get(
        &self,
        ctx: &Session,
        wk_name: &str,
        number: i64,
    ) -> Result<IssueResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(wk_name).await?;
        self.workspace_require_member(wk.id, user_uid).await?;

        let issue = self.issue_resolve(wk.id, number).await?;
        self.issue_build_response(issue).await
    }

    pub async fn issue_update(
        &self,
        ctx: &Session,
        wk_name: &str,
        number: i64,
        params: UpdateIssue,
    ) -> Result<IssueResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(wk_name).await?;
        self.workspace_require_member(wk.id, user_uid).await?;
        let mut issue = self.issue_resolve(wk.id, number).await?;

        let now = chrono::Utc::now();
        let mut txn = self.db.begin().await.map_err(|_| AppError::TxnError)?;

        if let Some(title) = &params.title {
            let title = title.trim();
            if title.is_empty() {
                return Err(AppError::BadRequest(
                    "issue title is required".to_string(),
                ));
            }
            if title != &issue.title {
                sqlx::query(
                    "INSERT INTO issue_event (id, issue, actor, event, from_value, to_value, created_at) \
                     VALUES ($1, $2, $3, 'title_changed', $4, $5, $6)",
                )
                .bind(uuid::Uuid::now_v7())
                .bind(issue.id)
                .bind(user_uid)
                .bind(&issue.title)
                .bind(title)
                .bind(now)
                .execute(&mut **txn.inner_mut())
                .await
                .map_err(|e| AppError::DatabaseError(e.to_string()))?;
                issue.title = title.to_string();
            }
        }

        if let Some(priority) = &params.priority {
            if priority != &issue.priority {
                sqlx::query(
                    "INSERT INTO issue_event (id, issue, actor, event, from_value, to_value, created_at) \
                     VALUES ($1, $2, $3, 'priority_changed', $4, $5, $6)",
                )
                .bind(uuid::Uuid::now_v7())
                .bind(issue.id)
                .bind(user_uid)
                .bind(&issue.priority)
                .bind(priority)
                .bind(now)
                .execute(&mut **txn.inner_mut())
                .await
                .map_err(|e| AppError::DatabaseError(e.to_string()))?;
                issue.priority = priority.to_string();
            }
        }

        let next_body = params.body.map(Some).unwrap_or(issue.body.clone());
        let next_due_at = params
            .due_at
            .and_then(|d| {
                chrono::DateTime::parse_from_rfc3339(&d)
                    .ok()
                    .map(|dt| dt.to_utc())
            })
            .or(issue.due_at);

        issue = sqlx::query_as::<_, IssueModel>(
            "UPDATE issue SET title = $1, body = $2, priority = $3, due_at = $4, updated_at = $5 WHERE id = $6 \
             RETURNING id, wk, number, title, body, state, priority, author, closed_by, closed_at, due_at, created_at, updated_at, deleted_at",
        )
        .bind(&issue.title)
        .bind(&next_body)
        .bind(&issue.priority)
        .bind(next_due_at)
        .bind(now)
        .bind(issue.id)
        .fetch_one(&mut **txn.inner_mut())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        txn.commit().await.map_err(|_| AppError::TxnError)?;
        self.issue_build_response(issue).await
    }

    pub async fn issue_close(
        &self,
        ctx: &Session,
        wk_name: &str,
        number: i64,
    ) -> Result<IssueResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(wk_name).await?;
        self.workspace_require_member(wk.id, user_uid).await?;
        let issue = self.issue_resolve(wk.id, number).await?;

        if issue.state == "closed" {
            return Err(AppError::BadRequest(
                "issue is already closed".to_string(),
            ));
        }

        let now = chrono::Utc::now();
        sqlx::query(
            "UPDATE issue SET state = 'closed', closed_by = $1, closed_at = $2, updated_at = $2 WHERE id = $3",
        )
        .bind(user_uid)
        .bind(now)
        .bind(issue.id)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        sqlx::query(
            "INSERT INTO issue_event (id, issue, actor, event, from_value, to_value, created_at) \
             VALUES ($1, $2, $3, 'closed', $4, 'closed', $5)",
        )
        .bind(uuid::Uuid::now_v7())
        .bind(issue.id)
        .bind(user_uid)
        .bind(&issue.state)
        .bind(now)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        let issue = self.issue_resolve(wk.id, number).await?;
        self.issue_build_response(issue).await
    }

    pub async fn issue_reopen(
        &self,
        ctx: &Session,
        wk_name: &str,
        number: i64,
    ) -> Result<IssueResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(wk_name).await?;
        self.workspace_require_member(wk.id, user_uid).await?;
        let issue = self.issue_resolve(wk.id, number).await?;

        if issue.state == "open" {
            return Err(AppError::BadRequest(
                "issue is already open".to_string(),
            ));
        }

        let now = chrono::Utc::now();
        sqlx::query(
            "UPDATE issue SET state = 'open', closed_by = NULL, closed_at = NULL, updated_at = $1 WHERE id = $2",
        )
        .bind(now)
        .bind(issue.id)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        sqlx::query(
            "INSERT INTO issue_event (id, issue, actor, event, from_value, to_value, created_at) \
             VALUES ($1, $2, $3, 'reopened', $4, 'open', $5)",
        )
        .bind(uuid::Uuid::now_v7())
        .bind(issue.id)
        .bind(user_uid)
        .bind(&issue.state)
        .bind(now)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        let issue = self.issue_resolve(wk.id, number).await?;
        self.issue_build_response(issue).await
    }

    pub async fn issue_delete(
        &self,
        ctx: &Session,
        wk_name: &str,
        number: i64,
    ) -> Result<(), AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(wk_name).await?;
        self.workspace_require_admin(wk.id, user_uid).await?;
        let issue = self.issue_resolve(wk.id, number).await?;

        sqlx::query("UPDATE issue SET deleted_at = $1 WHERE id = $2")
            .bind(chrono::Utc::now())
            .bind(issue.id)
            .execute(self.db.writer())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(())
    }

    pub async fn issue_resolve(
        &self,
        wk_id: uuid::Uuid,
        number: i64,
    ) -> Result<IssueModel, AppError> {
        sqlx::query_as::<_, IssueModel>(
            "SELECT id, wk, number, title, body, state, priority, author, closed_by, closed_at, due_at, \
             created_at, updated_at, deleted_at \
             FROM issue WHERE wk = $1 AND number = $2 AND deleted_at IS NULL",
        )
        .bind(wk_id)
        .bind(number)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::IssueNotFound)
    }

    async fn issue_build_response(
        &self,
        issue: IssueModel,
    ) -> Result<IssueResponse, AppError> {
        let author = self.users_find_by_id(issue.author).await?;

        let closed_by = if let Some(closed_uid) = issue.closed_by {
            let user = self.users_find_by_id(closed_uid).await?;
            Some(issue_author(user))
        } else {
            None
        };

        let labels = self.issue_labels(issue.id).await?;
        let assignees = self.issue_assignees(issue.id).await?;
        let milestone = self.issue_milestone(issue.id).await?;
        let repos = self.issue_repos(issue.id).await?;
        let pull_requests = self.issue_pull_requests(issue.id).await?;

        Ok(IssueResponse {
            number: issue.number,
            title: issue.title,
            body: issue.body,
            state: issue.state,
            priority: issue.priority,
            due_at: issue.due_at,
            author: issue_author(author),
            closed_by,
            closed_at: issue.closed_at,
            created_at: issue.created_at,
            updated_at: issue.updated_at,
            labels,
            assignees,
            milestone,
            repos,
            pull_requests,
        })
    }

    pub async fn users_find_by_id(
        &self,
        uid: uuid::Uuid,
    ) -> Result<UserModel, AppError> {
        sqlx::query_as::<_, UserModel>(
            "SELECT id, username, display_name, avatar_url, website_url, allow_use, can_search, \
             last_sign_in_at, created_at, updated_at \
             FROM \"user\" WHERE id = $1 AND allow_use = true",
        )
        .bind(uid)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::UserNotFound)
    }

    /// CMDK BFF: list issue numbers, titles, states for a workspace.
    pub async fn issue_list_inner(
        &self,
        wk_name: &str,
    ) -> Result<Vec<(i32, String, String)>, AppError> {
        let wk = sqlx::query_as::<_, (uuid::Uuid,)>(
            "SELECT id FROM workspace WHERE name = $1",
        )
        .bind(wk_name)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or_else(|| AppError::NotFound("workspace not found".to_string()))?;

        let rows = sqlx::query_as::<_, (i32, String, String)>(
            "SELECT number, title, state FROM issue WHERE wk = $1 AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 50"
        )
        .bind(wk.0)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        Ok(rows)
    }
}
