use chrono::Utc;
use db::sqlx;
use model::repos::RepoCommitStatusModel;
use session::Session;
use uuid::Uuid;

use crate::AppService;
use crate::error::AppError;

#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct CommitStatusResponse {
    pub id: Uuid,
    pub commit_sha: String,
    pub state: String,
    pub target_url: Option<String>,
    pub description: Option<String>,
    pub context: String,
    pub creator: Uuid,
    pub created_at: chrono::DateTime<Utc>,
}

#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct CombinedCommitStatus {
    pub sha: String,
    pub state: String,
    pub total_count: i64,
    pub statuses: Vec<CommitStatusResponse>,
}

#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
pub struct CreateCommitStatus {
    pub state: String,
    pub target_url: Option<String>,
    pub description: Option<String>,
    pub context: Option<String>,
}

impl AppService {
    pub async fn git_commit_status_list(
        &self,
        repo_id: Uuid,
        commit_sha: &str,
    ) -> Result<Vec<CommitStatusResponse>, AppError> {
        let rows = sqlx::query_as::<_, RepoCommitStatusModel>(
            "SELECT id, repo, commit_sha, state, target_url, description, \
             context, creator, created_at, updated_at \
             FROM repo_commit_status \
             WHERE repo = $1 AND commit_sha = $2 \
             ORDER BY created_at DESC",
        )
        .bind(repo_id)
        .bind(commit_sha)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(rows.into_iter().map(status_to_response).collect())
    }

    pub async fn git_commit_status_combined(
        &self,
        repo_id: Uuid,
        commit_sha: &str,
    ) -> Result<CombinedCommitStatus, AppError> {
        let statuses = self.git_commit_status_list(repo_id, commit_sha).await?;
        let state = combined_state(&statuses);
        Ok(CombinedCommitStatus {
            sha: commit_sha.to_string(),
            state,
            total_count: statuses.len() as i64,
            statuses,
        })
    }

    pub async fn git_commit_status_create(
        &self,
        repo_id: Uuid,
        user_id: Uuid,
        commit_sha: &str,
        params: CreateCommitStatus,
    ) -> Result<CommitStatusResponse, AppError> {
        if !["pending", "success", "failure", "error"]
            .contains(&params.state.as_str())
        {
            return Err(AppError::BadRequest(
                "state must be one of: pending, success, failure, error"
                    .to_string(),
            ));
        }

        let id = Uuid::now_v7();
        let now = Utc::now();
        let context = params.context.unwrap_or_else(|| "default".to_string());

        let row = sqlx::query_as::<_, RepoCommitStatusModel>(
            "INSERT INTO repo_commit_status (id, repo, commit_sha, state, target_url, \
             description, context, creator, created_at, updated_at) \
             VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$9) RETURNING *",
        )
        .bind(id)
        .bind(repo_id)
        .bind(commit_sha)
        .bind(&params.state)
        .bind(&params.target_url)
        .bind(&params.description)
        .bind(&context)
        .bind(user_id)
        .bind(now)
        .fetch_one(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(status_to_response(row))
    }
}

impl AppService {
    pub async fn git_commit_status_list_by_name(
        &self,
        ctx: &Session,
        wk: &str,
        repo: &str,
        sha: &str,
    ) -> Result<Vec<CommitStatusResponse>, AppError> {
        let repo = self.git_require_member(ctx, wk, repo).await?;
        self.git_commit_status_list(repo.id, sha).await
    }
    pub async fn git_commit_status_combined_by_name(
        &self,
        ctx: &Session,
        wk: &str,
        repo: &str,
        sha: &str,
    ) -> Result<CombinedCommitStatus, AppError> {
        let repo = self.git_require_member(ctx, wk, repo).await?;
        self.git_commit_status_combined(repo.id, sha).await
    }
    pub async fn git_commit_status_create_by_name(
        &self,
        ctx: &Session,
        user_id: Uuid,
        wk: &str,
        repo: &str,
        sha: &str,
        params: CreateCommitStatus,
    ) -> Result<CommitStatusResponse, AppError> {
        let repo = self.git_require_member(ctx, wk, repo).await?;
        self.git_commit_status_create(repo.id, user_id, sha, params)
            .await
    }
}

fn status_to_response(s: RepoCommitStatusModel) -> CommitStatusResponse {
    CommitStatusResponse {
        id: s.id,
        commit_sha: s.commit_sha,
        state: s.state,
        target_url: s.target_url,
        description: s.description,
        context: s.context,
        creator: s.creator,
        created_at: s.created_at,
    }
}

fn combined_state(statuses: &[CommitStatusResponse]) -> String {
    if statuses.is_empty() {
        return "pending".to_string();
    }
    let has = |s: &str| statuses.iter().any(|st| st.state == s);
    (if has("error") {
        "error"
    } else if has("failure") {
        "failure"
    } else if has("pending") {
        "pending"
    } else {
        "success"
    })
    .to_string()
}
