use crate::hook::sync::commit::TagTip;
use crate::GitError;
use crate::hook::sync::HookMetaDataSync;
use db::database::AppTransaction;
use models::repos::repo_tag;
use sea_orm::prelude::Expr;
use sea_orm::*;
use std::collections::HashSet;

impl HookMetaDataSync {
    /// Collect all tag metadata from git2 into owned structs.
    /// This is sync and must be called from a `spawn_blocking` context.
    pub(crate) fn collect_tag_refs(&self) -> Result<Vec<TagTip>, GitError> {
        let repo = self.domain.repo();
        let tag_names = repo
            .tag_names(None)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let mut tags = Vec::new();
        for tag_name in tag_names.iter().flatten() {
            let full_ref = format!("refs/tags/{}", tag_name);
            let reference = match repo.find_reference(&full_ref) {
                Ok(r) => r,
                Err(_) => continue,
            };

            let target_oid = match reference.target() {
                Some(oid) => oid.to_string(),
                None => continue,
            };

            let (description, tagger_name, tagger_email) = if reference.is_tag() {
                if let Ok(tag) = reference.peel_to_tag() {
                    let description = tag.message().map(|s| s.to_string());
                    if let Some(tagger) = tag.tagger() {
                        (
                            description,
                            tagger.name().unwrap_or("").to_string(),
                            tagger.email().unwrap_or("").to_string(),
                        )
                    } else {
                        (description, String::new(), String::new())
                    }
                } else {
                    (None, String::new(), String::new())
                }
            } else {
                (None, String::new(), String::new())
            };

            tags.push(TagTip {
                name: tag_name.to_string(),
                target_oid,
                description,
                tagger_name,
                tagger_email,
            });
        }

        Ok(tags)
    }

    pub async fn sync_tags(&self, txn: &AppTransaction) -> Result<(), GitError> {
        let repo_id = self.repo.id;

        let existing: Vec<repo_tag::Model> = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo_id))
            .all(txn)
            .await
            .map_err(|e| GitError::IoError(format!("failed to query tags: {}", e)))?;
        let mut existing_names: HashSet<String> = existing.iter().map(|t| t.name.clone()).collect();

        let tags = self.collect_tag_refs()?;

        for tag in tags {
            if existing_names.contains(&tag.name) {
                existing_names.remove(&tag.name);
                repo_tag::Entity::update_many()
                    .filter(repo_tag::Column::Repo.eq(repo_id))
                    .filter(repo_tag::Column::Name.eq(&tag.name))
                    .col_expr(repo_tag::Column::Oid, Expr::value(&tag.target_oid))
                    .col_expr(repo_tag::Column::Description, Expr::value(tag.description.clone()))
                    .col_expr(repo_tag::Column::TaggerName, Expr::value(&tag.tagger_name))
                    .col_expr(repo_tag::Column::TaggerEmail, Expr::value(&tag.tagger_email))
                    .exec(txn)
                    .await
                    .map_err(|e| GitError::IoError(format!("failed to update tag: {}", e)))?;
            } else {
                let new_tag = repo_tag::ActiveModel {
                    repo: Set(repo_id),
                    name: Set(tag.name.clone()),
                    oid: Set(tag.target_oid),
                    color: Set(None),
                    description: Set(tag.description.clone()),
                    created_at: Set(chrono::Utc::now()),
                    tagger_name: Set(tag.tagger_name.clone()),
                    tagger_email: Set(tag.tagger_email.clone()),
                    tagger: Set(None),
                    ..Default::default()
                };
                new_tag
                    .insert(txn)
                    .await
                    .map_err(|e| GitError::IoError(format!("failed to insert tag: {}", e)))?;
            }
        }

        if !existing_names.is_empty() {
            repo_tag::Entity::delete_many()
                .filter(repo_tag::Column::Repo.eq(repo_id))
                .filter(repo_tag::Column::Name.is_in(existing_names))
                .exec(txn)
                .await
                .map_err(|e| GitError::IoError(format!("failed to delete stale tags: {}", e)))?;
        }

        Ok(())
    }
}
