use std::collections::HashSet;

use db::{database::AppDatabase, sqlx};
use model::repos::RepoRefModel;
use uuid::Uuid;

use crate::{bare::GitBare, errors::GitError};
#[derive(Debug, Clone)]
pub struct TagTip {
    pub name: String,
    pub target_oid: String,
}
pub fn collect_tag_tips(bare: &GitBare) -> Result<Vec<TagTip>, GitError> {
    let repo = bare.gix_repo()?;
    let refs = repo.references().map_err(|e| {
        GitError::Internal(format!("failed to open references: {}", e))
    })?;
    let iter = refs.all().map_err(|e| {
        GitError::Internal(format!("failed to iterate refs: {}", e))
    })?;

    let mut tags = Vec::new();
    for ref_result in iter {
        let reference = ref_result.map_err(|e| {
            GitError::Internal(format!("ref iteration error: {}", e))
        })?;
        let full_name = reference.name().as_bstr().to_string();
        if !full_name.starts_with("refs/tags/") {
            continue;
        }
        let target_oid = reference
            .target()
            .try_id()
            .map(|id| id.to_hex().to_string())
            .ok_or_else(|| {
                GitError::Internal("ref has no direct target".to_string())
            })?;
        let short_name = reference.name().shorten().to_string();
        tags.push(TagTip {
            name: short_name,
            target_oid,
        });
    }
    Ok(tags)
}
pub async fn sync_tags(
    db: &AppDatabase,
    bare: &GitBare,
    repo_id: Uuid,
) -> Result<(), GitError> {
    let pool = db.writer();

    let existing: Vec<RepoRefModel> = sqlx::query_as::<_, RepoRefModel>(
        "SELECT id, repo, name, kind, target_sha, is_default, is_protected, created_at, updated_at FROM repo_ref WHERE repo = $1 AND kind = 'tag'"
    )
        .bind(repo_id)
        .fetch_all(pool)
        .await
        .map_err(|e| GitError::Internal(format!("failed to query tags: {}", e)))?;
    let mut existing_names: HashSet<String> =
        existing.iter().map(|t| t.name.clone()).collect();

    let tags = collect_tag_tips(bare)?;
    let now = chrono::Utc::now();

    for tag in tags {
        if existing_names.contains(&tag.name) {
            existing_names.remove(&tag.name);
            sqlx::query(
                "UPDATE repo_ref SET target_sha = $1, updated_at = $2 WHERE repo = $3 AND name = $4 AND kind = 'tag'"
            )
                .bind(&tag.target_oid)
                .bind(now)
                .bind(repo_id)
                .bind(&tag.name)
                .execute(pool)
                .await
                .map_err(|e| GitError::Internal(format!("failed to update tag: {}", e)))?;
        } else {
            let new_id = Uuid::new_v4();
            sqlx::query(
                "INSERT INTO repo_ref (id, repo, name, kind, target_sha, is_default, is_protected, created_at, updated_at) VALUES ($1, $2, $3, 'tag', $4, false, false, $5, $6)"
            )
                .bind(new_id)
                .bind(repo_id)
                .bind(&tag.name)
                .bind(&tag.target_oid)
                .bind(now)
                .bind(now)
                .execute(pool)
                .await
                .map_err(|e| GitError::Internal(format!("failed to insert tag: {}", e)))?;
        }
    }

    if !existing_names.is_empty() {
        let names_vec: Vec<String> = existing_names.into_iter().collect();
        sqlx::query("DELETE FROM repo_ref WHERE repo = $1 AND name = ANY($2) AND kind = 'tag'")
            .bind(repo_id)
            .bind(&names_vec)
            .execute(pool)
            .await
            .map_err(|e| GitError::Internal(format!("failed to delete stale tags: {}", e)))?;
    }

    Ok(())
}
