use db::sqlx;
use model::issues::LabelModel;
use serde::Deserialize;
use session::Session;

use super::types::{LabelResponse, label_response};
use crate::{AppService, error::AppError, session_user};

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CreateLabel {
    pub name: String,
    pub color: String,
    pub description: Option<String>,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct UpdateLabel {
    pub name: Option<String>,
    pub color: Option<String>,
    pub description: Option<String>,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct AddIssueLabel {
    pub label_id: uuid::Uuid,
}

impl AppService {
    pub async fn label_create(
        &self,
        ctx: &Session,
        wk_name: &str,
        params: CreateLabel,
    ) -> Result<LabelResponse, 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 name = params.name.trim();
        if name.is_empty() {
            return Err(AppError::BadRequest(
                "label name is required".to_string(),
            ));
        }

        let id = uuid::Uuid::now_v7();
        let now = chrono::Utc::now();

        let label = sqlx::query_as::<_, LabelModel>(
            "INSERT INTO label (id, wk, name, color, description, created_at, updated_at) \
             VALUES ($1, $2, $3, $4, $5, $6, $6) \
             RETURNING id, wk, name, color, description, created_at, updated_at",
        )
        .bind(id)
        .bind(wk.id)
        .bind(name)
        .bind(&params.color)
        .bind(&params.description)
        .bind(now)
        .fetch_one(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(label_response(label))
    }

    pub async fn label_list(
        &self,
        ctx: &Session,
        wk_name: &str,
    ) -> Result<Vec<LabelResponse>, 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 labels = sqlx::query_as::<_, LabelModel>(
            "SELECT id, wk, name, color, description, created_at, updated_at \
             FROM label WHERE wk = $1 ORDER BY name ASC",
        )
        .bind(wk.id)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(labels.into_iter().map(label_response).collect())
    }

    pub async fn label_update(
        &self,
        ctx: &Session,
        wk_name: &str,
        label_id: uuid::Uuid,
        params: UpdateLabel,
    ) -> Result<LabelResponse, 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 label = sqlx::query_as::<_, LabelModel>(
            "SELECT id, wk, name, color, description, created_at, updated_at \
             FROM label WHERE id = $1 AND wk = $2",
        )
        .bind(label_id)
        .bind(wk.id)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::LabelNotFound)?;

        let name = params.name.unwrap_or_else(|| label.name.clone());
        let color = params.color.unwrap_or_else(|| label.color.clone());
        let description = params
            .description
            .unwrap_or_else(|| label.description.clone().unwrap_or_default());

        let now = chrono::Utc::now();
        let updated = sqlx::query_as::<_, LabelModel>(
            "UPDATE label SET name = $1, color = $2, description = $3, updated_at = $4 WHERE id = $5 \
             RETURNING id, wk, name, color, description, created_at, updated_at",
        )
        .bind(name)
        .bind(color)
        .bind(description)
        .bind(now)
        .bind(label_id)
        .fetch_one(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(label_response(updated))
    }

    pub async fn label_delete(
        &self,
        ctx: &Session,
        wk_name: &str,
        label_id: uuid::Uuid,
    ) -> 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 exists = sqlx::query_scalar::<_, bool>(
            "SELECT EXISTS(SELECT 1 FROM label WHERE id = $1 AND wk = $2)",
        )
        .bind(label_id)
        .bind(wk.id)
        .fetch_one(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        if !exists {
            return Err(AppError::LabelNotFound);
        }

        sqlx::query("DELETE FROM issue_label WHERE label = $1")
            .bind(label_id)
            .execute(self.db.writer())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        sqlx::query("DELETE FROM label WHERE id = $1")
            .bind(label_id)
            .execute(self.db.writer())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(())
    }

    pub async fn issue_add_label(
        &self,
        ctx: &Session,
        wk_name: &str,
        number: i64,
        params: AddIssueLabel,
    ) -> Result<Vec<LabelResponse>, 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?;

        let label = sqlx::query_as::<_, LabelModel>(
            "SELECT id, wk, name, color, description, created_at, updated_at \
             FROM label WHERE id = $1 AND wk = $2",
        )
        .bind(params.label_id)
        .bind(wk.id)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::LabelNotFound)?;

        let now = chrono::Utc::now();
        sqlx::query(
            "INSERT INTO issue_label (issue, label, created_at) VALUES ($1, $2, $3) \
             ON CONFLICT (issue, label) DO NOTHING",
        )
        .bind(issue.id)
        .bind(label.id)
        .bind(now)
        .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, 'labeled', NULL, $4, $5)",
        )
        .bind(uuid::Uuid::now_v7())
        .bind(issue.id)
        .bind(user_uid)
        .bind(&label.name)
        .bind(now)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        self.issue_labels(issue.id).await
    }

    pub async fn issue_remove_label(
        &self,
        ctx: &Session,
        wk_name: &str,
        number: i64,
        label_id: uuid::Uuid,
    ) -> Result<Vec<LabelResponse>, 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?;

        let label = sqlx::query_as::<_, LabelModel>(
            "SELECT id, wk, name, color, description, created_at, updated_at \
             FROM label WHERE id = $1 AND wk = $2",
        )
        .bind(label_id)
        .bind(wk.id)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::LabelNotFound)?;

        sqlx::query("DELETE FROM issue_label WHERE issue = $1 AND label = $2")
            .bind(issue.id)
            .bind(label_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, 'unlabeled', $4, NULL, $5)",
        )
        .bind(uuid::Uuid::now_v7())
        .bind(issue.id)
        .bind(user_uid)
        .bind(&label.name)
        .bind(chrono::Utc::now())
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        self.issue_labels(issue.id).await
    }

    pub async fn issue_labels(
        &self,
        issue_id: uuid::Uuid,
    ) -> Result<Vec<LabelResponse>, AppError> {
        let labels = sqlx::query_as::<_, LabelModel>(
            "SELECT l.id, l.wk, l.name, l.color, l.description, l.created_at, l.updated_at \
             FROM issue_label il \
             INNER JOIN label l ON l.id = il.label \
             WHERE il.issue = $1 \
             ORDER BY l.name ASC",
        )
        .bind(issue_id)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(labels.into_iter().map(label_response).collect())
    }
}
