use db::sqlx;
use model::workspace::{
    WkApplyJoinModel, WkJoinApprovalModel, WkJoinStrategyModel, WorkspaceModel,
};
use serde::{Deserialize, Serialize};
use session::Session;

use crate::{AppService, error::AppError, session_user};

const JOIN_STATUS_PENDING: &str = "pending";
const JOIN_STATUS_APPROVED: &str = "approved";
const JOIN_STATUS_REJECTED: &str = "rejected";
const JOIN_STATUS_CANCELLED: &str = "cancelled";

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct WorkspaceJoinStrategyResponse {
    pub workspace_name: String,
    pub workspace_avatar_url: String,
    pub require_approval: bool,
    pub require_question: bool,
    pub question: Option<String>,
    pub has_answer: bool,
    pub enabled: bool,
    #[schema(value_type = String)]
    pub created_at: chrono::DateTime<chrono::Utc>,
    #[schema(value_type = String)]
    pub updated_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct UpdateWorkspaceJoinStrategy {
    pub require_approval: Option<bool>,
    pub require_question: Option<bool>,
    pub question: Option<Option<String>>,
    pub answer: Option<Option<String>>,
    pub enabled: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct WorkspaceJoinApplyResponse {
    pub workspace_name: String,
    pub workspace_avatar_url: String,
    pub username: String,
    pub avatar_url: Option<String>,
    pub status: String,
    pub question: Option<String>,
    pub answer: Option<String>,
    pub message: Option<String>,
    #[schema(value_type = String)]
    pub created_at: chrono::DateTime<chrono::Utc>,
    #[schema(value_type = String)]
    pub updated_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CreateWorkspaceJoinApply {
    pub answer: Option<String>,
    pub message: Option<String>,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
pub struct ListWorkspaceJoinApply {
    pub status: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct WorkspaceJoinApprovalResponse {
    pub workspace_name: String,
    pub workspace_avatar_url: String,
    pub username: String,
    pub avatar_url: Option<String>,
    pub approver_username: String,
    pub approver_avatar_url: Option<String>,
    pub approved: bool,
    pub reason: Option<String>,
    #[schema(value_type = String)]
    pub created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct ApproveWorkspaceJoinApply {
    pub approved: bool,
    pub reason: Option<String>,
}

impl AppService {
    pub async fn workspace_join_strategy(
        &self,
        name: &str,
    ) -> Result<WorkspaceJoinStrategyResponse, AppError> {
        let wk = self.workspace_resolve(name).await?;
        let strategy = self.workspace_join_strategy_by_wk(wk.id).await?;
        Ok(strategy_response(strategy, &wk))
    }

    pub async fn workspace_update_join_strategy(
        &self,
        ctx: &Session,
        name: &str,
        params: UpdateWorkspaceJoinStrategy,
    ) -> Result<WorkspaceJoinStrategyResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(name).await?;
        self.workspace_require_admin(wk.id, user_uid).await?;

        let existing = self.workspace_join_strategy_by_wk(wk.id).await?;
        let require_approval =
            params.require_approval.unwrap_or(existing.require_approval);
        let mut require_question =
            params.require_question.unwrap_or(existing.require_question);
        let question = params.question.unwrap_or(existing.question);
        let answer = params.answer.unwrap_or(existing.answer);
        let enabled = params.enabled.unwrap_or(existing.enabled);

        if require_question
            && question
                .as_ref()
                .is_none_or(|question| question.trim().is_empty())
        {
            return Err(AppError::BadRequest(
                "join question is required when require_question is true"
                    .to_string(),
            ));
        }
        if question.is_none() {
            require_question = false;
        }

        let now = chrono::Utc::now();
        let saved = if self.workspace_join_strategy_exists(wk.id).await? {
            sqlx::query_as::<_, WkJoinStrategyModel>(
                "UPDATE wk_join_strategy SET require_approval = $1, require_question = $2, \
                    question = $3, answer = $4, enabled = $5, updated_at = $6 \
                 WHERE wk = $7 \
                 RETURNING wk, require_approval, require_question, question, answer, enabled, created_at, updated_at",
            )
            .bind(require_approval)
            .bind(require_question)
            .bind(clean_optional(question))
            .bind(clean_optional(answer))
            .bind(enabled)
            .bind(now)
            .bind(wk.id)
            .fetch_one(self.db.writer())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
        } else {
            sqlx::query_as::<_, WkJoinStrategyModel>(
                "INSERT INTO wk_join_strategy \
                    (wk, require_approval, require_question, question, answer, enabled, created_at, updated_at) \
                 VALUES ($1, $2, $3, $4, $5, $6, $7, $7) \
                 RETURNING wk, require_approval, require_question, question, answer, enabled, created_at, updated_at",
            )
            .bind(wk.id)
            .bind(require_approval)
            .bind(require_question)
            .bind(clean_optional(question))
            .bind(clean_optional(answer))
            .bind(enabled)
            .bind(now)
            .fetch_one(self.db.writer())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
        };

        Ok(strategy_response(saved, &wk))
    }

    pub async fn workspace_apply_join(
        &self,
        ctx: &Session,
        name: &str,
        params: CreateWorkspaceJoinApply,
    ) -> Result<WorkspaceJoinApplyResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(name).await?;
        if self.workspace_member(wk.id, user_uid).await.is_ok() {
            return Err(AppError::Conflict(
                "user is already a workspace member".to_string(),
            ));
        }
        if self
            .workspace_has_pending_join_apply(wk.id, user_uid)
            .await?
        {
            return Err(AppError::Conflict(
                "join application is already pending".to_string(),
            ));
        }

        let strategy = self.workspace_join_strategy_by_wk(wk.id).await?;
        let answer = clean_optional(params.answer);
        if strategy.enabled && strategy.require_question {
            let expected = strategy
                .answer
                .as_ref()
                .map(|answer| answer.trim())
                .unwrap_or_default();
            let actual = answer
                .as_ref()
                .map(|answer| answer.trim())
                .unwrap_or_default();
            if actual.is_empty() {
                return Err(AppError::BadRequest(
                    "join answer is required".to_string(),
                ));
            }
            if !expected.is_empty() && actual != expected {
                return Err(AppError::PermissionDenied);
            }
        }

        let status = if strategy.enabled && strategy.require_approval {
            JOIN_STATUS_PENDING
        } else {
            JOIN_STATUS_APPROVED
        };

        let now = chrono::Utc::now();
        let apply = sqlx::query_as::<_, WkApplyJoinModel>(
            "INSERT INTO wk_apply_join \
                (id, wk, \"user\", status, question, answer, message, created_at, updated_at) \
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8) \
             RETURNING id, wk, \"user\", status, question, answer, message, created_at, updated_at",
        )
        .bind(uuid::Uuid::now_v7())
        .bind(wk.id)
        .bind(user_uid)
        .bind(status)
        .bind(strategy.question.clone())
        .bind(answer)
        .bind(clean_optional(params.message))
        .bind(now)
        .fetch_one(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        if status == JOIN_STATUS_APPROVED {
            self.workspace_join_add_member(wk.id, user_uid).await?;
        }

        let current_user = self.auth_find_user_by_uid(user_uid).await?;
        Ok(apply_response(
            apply,
            &wk,
            current_user.username,
            clean_optional(Some(current_user.avatar_url)),
        ))
    }

    pub async fn workspace_my_join_applies(
        &self,
        ctx: &Session,
    ) -> Result<Vec<WorkspaceJoinApplyResponse>, AppError> {
        let user_uid = session_user(ctx)?;
        let rows = sqlx::query_as::<_, WorkspaceJoinApplyRow>(
            "SELECT a.id, a.wk, w.name AS workspace_name, w.avatar_url AS workspace_avatar_url, \
                    a.\"user\", u.username, u.avatar_url, a.status, a.question, a.answer, a.message, a.created_at, a.updated_at \
             FROM wk_apply_join a \
             INNER JOIN workspace w ON w.id = a.wk \
             INNER JOIN \"user\" u ON u.id = a.\"user\" \
             WHERE a.\"user\" = $1 ORDER BY a.created_at DESC",
        )
        .bind(user_uid)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(rows
            .into_iter()
            .map(WorkspaceJoinApplyResponse::from)
            .collect())
    }

    pub async fn workspace_cancel_join_apply(
        &self,
        ctx: &Session,
        name: &str,
    ) -> Result<WorkspaceJoinApplyResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(name).await?;
        let user = self.auth_find_user_by_uid(user_uid).await?;
        let apply = sqlx::query_as::<_, WkApplyJoinModel>(
            "UPDATE wk_apply_join SET status = $1, updated_at = $2 \
             WHERE wk = $3 AND \"user\" = $4 AND status = $5 \
             RETURNING id, wk, \"user\", status, question, answer, message, created_at, updated_at",
        )
        .bind(JOIN_STATUS_CANCELLED)
        .bind(chrono::Utc::now())
        .bind(wk.id)
        .bind(user_uid)
        .bind(JOIN_STATUS_PENDING)
        .fetch_optional(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::NotFound("join application not found".to_string()))?;

        Ok(apply_response(
            apply,
            &wk,
            user.username,
            clean_optional(Some(user.avatar_url)),
        ))
    }

    pub async fn workspace_join_applies(
        &self,
        ctx: &Session,
        name: &str,
        query: ListWorkspaceJoinApply,
    ) -> Result<Vec<WorkspaceJoinApplyResponse>, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(name).await?;
        self.workspace_require_admin(wk.id, user_uid).await?;
        let status = query
            .status
            .unwrap_or_else(|| JOIN_STATUS_PENDING.to_string());

        let rows = sqlx::query_as::<_, WorkspaceJoinApplyRow>(
            "SELECT a.id, a.wk, w.name AS workspace_name, w.avatar_url AS workspace_avatar_url, \
                    a.\"user\", u.username, u.avatar_url, a.status, a.question, a.answer, a.message, a.created_at, a.updated_at \
             FROM wk_apply_join a \
             INNER JOIN workspace w ON w.id = a.wk \
             INNER JOIN \"user\" u ON u.id = a.\"user\" \
             WHERE a.wk = $1 AND a.status = $2 \
             ORDER BY a.created_at ASC",
        )
        .bind(wk.id)
        .bind(status)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(rows
            .into_iter()
            .map(WorkspaceJoinApplyResponse::from)
            .collect())
    }

    pub async fn workspace_approve_join_apply(
        &self,
        ctx: &Session,
        name: &str,
        username: &str,
        params: ApproveWorkspaceJoinApply,
    ) -> Result<WorkspaceJoinApprovalResponse, AppError> {
        let approver = session_user(ctx)?;
        let wk = self.workspace_resolve(name).await?;
        self.workspace_require_admin(wk.id, approver).await?;
        let applicant =
            self.users_find_active_user_by_username(username).await?;
        let approver_user = self.auth_find_user_by_uid(approver).await?;
        let apply = self
            .workspace_pending_join_apply_by_user(wk.id, applicant.id)
            .await?;
        if apply.status != JOIN_STATUS_PENDING {
            return Err(AppError::Conflict(
                "join application has already been processed".to_string(),
            ));
        }

        let now = chrono::Utc::now();
        let approval_id = uuid::Uuid::now_v7();
        let next_status = if params.approved {
            JOIN_STATUS_APPROVED
        } else {
            JOIN_STATUS_REJECTED
        };

        let mut txn = self.db.begin().await.map_err(|_| AppError::TxnError)?;
        let approval = sqlx::query_as::<_, WkJoinApprovalModel>(
            "INSERT INTO wk_join_approval \
                (id, apply, wk, \"user\", approver, approved, reason, created_at) \
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8) \
             RETURNING id, apply, wk, \"user\", approver, approved, reason, created_at",
        )
        .bind(approval_id)
        .bind(apply.id)
        .bind(wk.id)
        .bind(apply.user)
        .bind(approver)
        .bind(params.approved)
        .bind(clean_optional(params.reason))
        .bind(now)
        .fetch_one(&mut **txn.inner_mut())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        sqlx::query("UPDATE wk_apply_join SET status = $1, updated_at = $2 WHERE id = $3")
            .bind(next_status)
            .bind(now)
            .bind(apply.id)
            .execute(&mut **txn.inner_mut())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        if params.approved {
            sqlx::query(
                "INSERT INTO wk_member (wk, \"user\", owner, admin, join_at, leave_at) \
                 VALUES ($1, $2, false, false, $3, NULL) \
                 ON CONFLICT (wk, \"user\") DO UPDATE SET leave_at = NULL",
            )
            .bind(wk.id)
            .bind(apply.user)
            .bind(now)
            .execute(&mut **txn.inner_mut())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        }

        txn.commit().await.map_err(|_| AppError::TxnError)?;
        Ok(approval_response(
            approval,
            &wk,
            applicant.username,
            clean_optional(Some(applicant.avatar_url)),
            approver_user.username,
            clean_optional(Some(approver_user.avatar_url)),
        ))
    }

    async fn workspace_join_strategy_by_wk(
        &self,
        wk: uuid::Uuid,
    ) -> Result<WkJoinStrategyModel, AppError> {
        let strategy = sqlx::query_as::<_, WkJoinStrategyModel>(
            "SELECT wk, require_approval, require_question, question, answer, enabled, created_at, updated_at \
             FROM wk_join_strategy WHERE wk = $1",
        )
        .bind(wk)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        let now = chrono::Utc::now();
        Ok(strategy.unwrap_or(WkJoinStrategyModel {
            wk,
            require_approval: false,
            require_question: false,
            question: None,
            answer: None,
            enabled: false,
            created_at: now,
            updated_at: now,
        }))
    }

    async fn workspace_join_strategy_exists(
        &self,
        wk: uuid::Uuid,
    ) -> Result<bool, AppError> {
        sqlx::query_scalar::<_, bool>(
            "SELECT EXISTS(SELECT 1 FROM wk_join_strategy WHERE wk = $1)",
        )
        .bind(wk)
        .fetch_one(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))
    }

    async fn workspace_has_pending_join_apply(
        &self,
        wk: uuid::Uuid,
        user: uuid::Uuid,
    ) -> Result<bool, AppError> {
        sqlx::query_scalar::<_, bool>(
            "SELECT EXISTS(SELECT 1 FROM wk_apply_join WHERE wk = $1 AND \"user\" = $2 AND status = $3)",
        )
        .bind(wk)
        .bind(user)
        .bind(JOIN_STATUS_PENDING)
        .fetch_one(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))
    }

    async fn workspace_pending_join_apply_by_user(
        &self,
        wk: uuid::Uuid,
        user: uuid::Uuid,
    ) -> Result<WkApplyJoinModel, AppError> {
        sqlx::query_as::<_, WkApplyJoinModel>(
            "SELECT id, wk, \"user\", status, question, answer, message, created_at, updated_at \
             FROM wk_apply_join WHERE wk = $1 AND \"user\" = $2 AND status = $3",
        )
        .bind(wk)
        .bind(user)
        .bind(JOIN_STATUS_PENDING)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::NotFound("join application not found".to_string()))
    }

    async fn workspace_join_add_member(
        &self,
        wk: uuid::Uuid,
        user: uuid::Uuid,
    ) -> Result<(), AppError> {
        sqlx::query(
            "INSERT INTO wk_member (wk, \"user\", owner, admin, join_at, leave_at) \
             VALUES ($1, $2, false, false, $3, NULL) \
             ON CONFLICT (wk, \"user\") DO UPDATE SET leave_at = NULL",
        )
        .bind(wk)
        .bind(user)
        .bind(chrono::Utc::now())
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        Ok(())
    }
}

#[derive(db::sqlx::FromRow)]
struct WorkspaceJoinApplyRow {
    workspace_name: String,
    workspace_avatar_url: String,
    username: String,
    avatar_url: String,
    status: String,
    question: Option<String>,
    answer: Option<String>,
    message: Option<String>,
    created_at: chrono::DateTime<chrono::Utc>,
    updated_at: chrono::DateTime<chrono::Utc>,
}

fn strategy_response(
    value: WkJoinStrategyModel,
    wk: &WorkspaceModel,
) -> WorkspaceJoinStrategyResponse {
    WorkspaceJoinStrategyResponse {
        workspace_name: wk.name.clone(),
        workspace_avatar_url: wk.avatar_url.clone(),
        require_approval: value.require_approval,
        require_question: value.require_question,
        question: value.question,
        has_answer: value.answer.is_some(),
        enabled: value.enabled,
        created_at: value.created_at,
        updated_at: value.updated_at,
    }
}

fn apply_response(
    value: WkApplyJoinModel,
    wk: &WorkspaceModel,
    username: String,
    avatar_url: Option<String>,
) -> WorkspaceJoinApplyResponse {
    WorkspaceJoinApplyResponse {
        workspace_name: wk.name.clone(),
        workspace_avatar_url: wk.avatar_url.clone(),
        username,
        avatar_url,
        status: value.status,
        question: value.question,
        answer: value.answer,
        message: value.message,
        created_at: value.created_at,
        updated_at: value.updated_at,
    }
}

fn approval_response(
    value: WkJoinApprovalModel,
    wk: &WorkspaceModel,
    username: String,
    avatar_url: Option<String>,
    approver_username: String,
    approver_avatar_url: Option<String>,
) -> WorkspaceJoinApprovalResponse {
    WorkspaceJoinApprovalResponse {
        workspace_name: wk.name.clone(),
        workspace_avatar_url: wk.avatar_url.clone(),
        username,
        avatar_url,
        approver_username,
        approver_avatar_url,
        approved: value.approved,
        reason: value.reason,
        created_at: value.created_at,
    }
}

fn clean_optional(value: Option<String>) -> Option<String> {
    value.and_then(|value| {
        let value = value.trim().to_string();
        if value.is_empty() { None } else { Some(value) }
    })
}

impl From<WorkspaceJoinApplyRow> for WorkspaceJoinApplyResponse {
    fn from(value: WorkspaceJoinApplyRow) -> Self {
        Self {
            workspace_name: value.workspace_name,
            workspace_avatar_url: value.workspace_avatar_url,
            username: value.username,
            avatar_url: clean_optional(Some(value.avatar_url)),
            status: value.status,
            question: value.question,
            answer: value.answer,
            message: value.message,
            created_at: value.created_at,
            updated_at: value.updated_at,
        }
    }
}
