use chrono::Utc;
use db::sqlx;
use model::repos::{RepoReleaseAssetModel, RepoReleaseModel};
use session::Session;
use uuid::Uuid;

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

#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct ReleaseResponse {
    pub id: Uuid,
    pub tag_name: String,
    pub target_commit_sha: String,
    pub name: String,
    pub body: Option<String>,
    pub draft: bool,
    pub prerelease: bool,
    pub author: Uuid,
    pub assets: Vec<ReleaseAssetResponse>,
    pub published_at: Option<chrono::DateTime<Utc>>,
    pub created_at: chrono::DateTime<Utc>,
}

#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct ReleaseAssetResponse {
    pub id: Uuid,
    pub name: String,
    pub content_type: Option<String>,
    pub size: i64,
    pub download_count: i64,
    pub created_at: chrono::DateTime<Utc>,
}

#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
pub struct CreateRelease {
    pub tag_name: String,
    pub target_commit_sha: Option<String>,
    pub name: String,
    pub body: Option<String>,
    #[serde(default)]
    pub draft: bool,
    #[serde(default)]
    pub prerelease: bool,
}

#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
pub struct UpdateRelease {
    pub tag_name: Option<String>,
    pub name: Option<String>,
    pub body: Option<Option<String>>,
    pub draft: Option<bool>,
    pub prerelease: Option<bool>,
}

impl AppService {
    pub async fn git_release_list(
        &self,
        repo_id: Uuid,
    ) -> Result<Vec<ReleaseResponse>, AppError> {
        let releases = sqlx::query_as::<_, RepoReleaseModel>(
            "SELECT id, repo, tag_name, target_commit_sha, name, body, \
             draft, prerelease, author, published_at, created_at, updated_at \
             FROM repo_release WHERE repo = $1 ORDER BY created_at DESC LIMIT 50",
        )
        .bind(repo_id)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        let mut result = Vec::with_capacity(releases.len());
        for r in releases {
            let assets = self.git_release_assets(r.id).await?;
            result.push(release_to_response(r, assets));
        }
        Ok(result)
    }

    pub async fn git_release_get(
        &self,
        repo_id: Uuid,
        release_id: Uuid,
    ) -> Result<ReleaseResponse, AppError> {
        let r = sqlx::query_as::<_, RepoReleaseModel>(
            "SELECT id, repo, tag_name, target_commit_sha, name, body, \
             draft, prerelease, author, published_at, created_at, updated_at \
             FROM repo_release WHERE id = $1 AND repo = $2",
        )
        .bind(release_id)
        .bind(repo_id)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or_else(|| AppError::NotFound("release not found".to_string()))?;

        let assets = self.git_release_assets(r.id).await?;
        Ok(release_to_response(r, assets))
    }

    pub async fn git_release_get_by_tag(
        &self,
        repo_id: Uuid,
        tag: &str,
    ) -> Result<ReleaseResponse, AppError> {
        let r = sqlx::query_as::<_, RepoReleaseModel>(
            "SELECT id, repo, tag_name, target_commit_sha, name, body, \
             draft, prerelease, author, published_at, created_at, updated_at \
             FROM repo_release WHERE repo = $1 AND tag_name = $2",
        )
        .bind(repo_id)
        .bind(tag)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or_else(|| AppError::NotFound("release not found".to_string()))?;

        let assets = self.git_release_assets(r.id).await?;
        Ok(release_to_response(r, assets))
    }

    pub async fn git_release_create(
        &self,
        _session: &Session,
        repo_id: Uuid,
        user_id: Uuid,
        params: CreateRelease,
    ) -> Result<ReleaseResponse, AppError> {
        let id = Uuid::now_v7();
        let now = Utc::now();
        let published_at = if params.draft { None } else { Some(now) };

        let target = if let Some(ref sha) = params.target_commit_sha {
            if !sha.trim().is_empty() {
                sha.clone()
            } else {
                self.default_branch_sha(repo_id).await?
            }
        } else {
            self.default_branch_sha(repo_id).await?
        };

        let r = sqlx::query_as::<_, RepoReleaseModel>(
            "INSERT INTO repo_release (id, repo, tag_name, target_commit_sha, name, body, \
             draft, prerelease, author, published_at, created_at, updated_at) \
             VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$11) \
             RETURNING id, repo, tag_name, target_commit_sha, name, body, \
             draft, prerelease, author, published_at, created_at, updated_at",
        )
        .bind(id)
        .bind(repo_id)
        .bind(&params.tag_name)
        .bind(&target)
        .bind(&params.name)
        .bind(&params.body)
        .bind(params.draft)
        .bind(params.prerelease)
        .bind(user_id)
        .bind(published_at)
        .bind(now)
        .fetch_one(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(release_to_response(r, Vec::new()))
    }

    pub async fn git_release_update(
        &self,
        repo_id: Uuid,
        release_id: Uuid,
        params: UpdateRelease,
    ) -> Result<ReleaseResponse, AppError> {
        let existing = self.git_release_get(repo_id, release_id).await?;
        let now = Utc::now();

        let tag_name = params.tag_name.unwrap_or(existing.tag_name);
        let name = params.name.unwrap_or(existing.name);
        let body = params.body.unwrap_or(existing.body);
        let draft = params.draft.unwrap_or(existing.draft);
        let prerelease = params.prerelease.unwrap_or(existing.prerelease);

        let published_at = if draft {
            None
        } else {
            existing.published_at.or(Some(now))
        };

        let r = sqlx::query_as::<_, RepoReleaseModel>(
            "UPDATE repo_release SET tag_name=$1, name=$2, body=$3, draft=$4, \
             prerelease=$5, published_at=$6, updated_at=$7 \
             WHERE id=$8 AND repo=$9 \
             RETURNING id, repo, tag_name, target_commit_sha, name, body, \
             draft, prerelease, author, published_at, created_at, updated_at",
        )
        .bind(&tag_name)
        .bind(&name)
        .bind(&body)
        .bind(draft)
        .bind(prerelease)
        .bind(published_at)
        .bind(now)
        .bind(release_id)
        .bind(repo_id)
        .fetch_one(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        let assets = self.git_release_assets(r.id).await?;
        Ok(release_to_response(r, assets))
    }

    pub async fn git_release_delete(
        &self,
        repo_id: Uuid,
        release_id: Uuid,
    ) -> Result<(), AppError> {
        let rows =
            sqlx::query("DELETE FROM repo_release WHERE id = $1 AND repo = $2")
                .bind(release_id)
                .bind(repo_id)
                .execute(self.db.writer())
                .await
                .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        if rows.rows_affected() == 0 {
            return Err(AppError::NotFound("release not found".to_string()));
        }
        Ok(())
    }

    pub async fn git_release_delete_by_tag(
        &self,
        repo_id: Uuid,
        tag: &str,
    ) -> Result<(), AppError> {
        let release_id: Option<Uuid> = sqlx::query_scalar(
            "SELECT id FROM repo_release WHERE repo = $1 AND tag_name = $2",
        )
        .bind(repo_id)
        .bind(tag)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        let release_id = release_id.ok_or_else(|| {
            AppError::NotFound("release not found".to_string())
        })?;
        self.git_release_delete(repo_id, release_id).await
    }

    async fn git_release_assets(
        &self,
        release_id: Uuid,
    ) -> Result<Vec<ReleaseAssetResponse>, AppError> {
        let assets = sqlx::query_as::<_, RepoReleaseAssetModel>(
            "SELECT id, release_id, name, content_type, size, download_count, \
             storage_path, uploader, created_at \
             FROM repo_release_asset WHERE release_id = $1 ORDER BY created_at",
        )
        .bind(release_id)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(assets
            .into_iter()
            .map(|a| ReleaseAssetResponse {
                id: a.id,
                name: a.name,
                content_type: a.content_type,
                size: a.size,
                download_count: a.download_count,
                created_at: a.created_at,
            })
            .collect())
    }

    pub async fn git_release_asset_create(
        &self,
        repo_id: Uuid,
        release_id: Uuid,
        user_id: Uuid,
        name: String,
        content_type: Option<String>,
        size: i64,
        storage_path: String,
    ) -> Result<ReleaseAssetResponse, AppError> {
        let _ = self.git_release_get(repo_id, release_id).await?;

        let id = Uuid::now_v7();
        let now = Utc::now();
        let a = sqlx::query_as::<_, RepoReleaseAssetModel>(
            "INSERT INTO repo_release_asset (id, release_id, name, content_type, size, \
             download_count, storage_path, uploader, created_at) \
             VALUES ($1,$2,$3,$4,$5,0,$6,$7,$8) RETURNING *",
        )
        .bind(id)
        .bind(release_id)
        .bind(&name)
        .bind(&content_type)
        .bind(size)
        .bind(&storage_path)
        .bind(user_id)
        .bind(now)
        .fetch_one(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(ReleaseAssetResponse {
            id: a.id,
            name: a.name,
            content_type: a.content_type,
            size: a.size,
            download_count: a.download_count,
            created_at: a.created_at,
        })
    }

    pub async fn git_release_asset_delete(
        &self,
        repo_id: Uuid,
        release_id: Uuid,
        asset_id: Uuid,
    ) -> Result<(), AppError> {
        let _ = self.git_release_get(repo_id, release_id).await?;
        sqlx::query(
            "DELETE FROM repo_release_asset WHERE id = $1 AND release_id = $2",
        )
        .bind(asset_id)
        .bind(release_id)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        Ok(())
    }
}

impl AppService {
    pub async fn git_release_list_by_name(
        &self,
        ctx: &Session,
        _user_id: Uuid,
        wk: &str,
        repo: &str,
    ) -> Result<Vec<ReleaseResponse>, AppError> {
        let repo = self.git_require_member(ctx, wk, repo).await?;
        self.git_release_list(repo.id).await
    }
    pub async fn git_release_get_by_name(
        &self,
        ctx: &Session,
        _user_id: Uuid,
        wk: &str,
        repo: &str,
        id: Uuid,
    ) -> Result<ReleaseResponse, AppError> {
        let repo = self.git_require_member(ctx, wk, repo).await?;
        self.git_release_get(repo.id, id).await
    }
    pub async fn git_release_get_by_tag_name(
        &self,
        ctx: &Session,
        _user_id: Uuid,
        wk: &str,
        repo: &str,
        tag: &str,
    ) -> Result<ReleaseResponse, AppError> {
        let repo = self.git_require_member(ctx, wk, repo).await?;
        self.git_release_get_by_tag(repo.id, tag).await
    }
    pub async fn git_release_create_by_name(
        &self,
        ctx: &Session,
        user_id: Uuid,
        wk: &str,
        repo: &str,
        params: CreateRelease,
    ) -> Result<ReleaseResponse, AppError> {
        let repo = self.git_require_member(ctx, wk, repo).await?;
        self.git_release_create(ctx, repo.id, user_id, params).await
    }
    pub async fn git_release_update_by_name(
        &self,
        ctx: &Session,
        _user_id: Uuid,
        wk: &str,
        repo: &str,
        id: Uuid,
        params: UpdateRelease,
    ) -> Result<ReleaseResponse, AppError> {
        let repo = self.git_require_member(ctx, wk, repo).await?;
        self.git_release_update(repo.id, id, params).await
    }
    pub async fn git_release_delete_by_name(
        &self,
        ctx: &Session,
        _user_id: Uuid,
        wk: &str,
        repo: &str,
        id: Uuid,
    ) -> Result<(), AppError> {
        let repo = self.git_require_member(ctx, wk, repo).await?;
        self.git_release_delete(repo.id, id).await
    }
    pub async fn git_release_delete_by_tag_name(
        &self,
        ctx: &Session,
        _user_id: Uuid,
        wk: &str,
        repo: &str,
        tag: &str,
    ) -> Result<(), AppError> {
        let repo = self.git_require_member(ctx, wk, repo).await?;
        self.git_release_delete_by_tag(repo.id, tag).await
    }
}

impl AppService {
    async fn default_branch_sha(
        &self,
        repo_id: Uuid,
    ) -> Result<String, AppError> {
        sqlx::query_scalar("SELECT target_sha FROM repo_ref WHERE repo = $1 AND is_default = true")
            .bind(repo_id)
            .fetch_optional(self.db.reader())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
            .ok_or_else(|| AppError::BadRequest("no default branch to target".to_string()))
    }
}

fn release_to_response(
    r: RepoReleaseModel,
    assets: Vec<ReleaseAssetResponse>,
) -> ReleaseResponse {
    ReleaseResponse {
        id: r.id,
        tag_name: r.tag_name,
        target_commit_sha: r.target_commit_sha,
        name: r.name,
        body: r.body,
        draft: r.draft,
        prerelease: r.prerelease,
        author: r.author,
        assets,
        published_at: r.published_at,
        created_at: r.created_at,
    }
}
