//! Tag create/delete/rename operations.

use crate::commit::types::{CommitOid, CommitSignature};
use crate::ref_utils::validate_ref_name;
use crate::tags::types::TagInfo;
use crate::{GitDomain, GitError, GitResult};

impl GitDomain {
    pub fn tag_create(
        &self,
        name: &str,
        target: &CommitOid,
        message: &str,
        tagger: &CommitSignature,
        force: bool,
    ) -> GitResult<TagInfo> {
        validate_ref_name(name)?;

        let target_oid = target
            .to_oid()
            .map_err(|_| GitError::InvalidOid(target.to_string()))?;

        let obj = self
            .repo()
            .find_object(target_oid, None)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let sig = self.commit_signature_to_git2(tagger)?;

        let tag_oid = self
            .repo()
            .tag(name, &obj, &sig, message, force)
            .map_err(|e| {
                if e.code() == git2::ErrorCode::Exists {
                    GitError::TagExists(name.to_string())
                } else {
                    GitError::Internal(e.to_string())
                }
            })?;

        let ref_name = format!("refs/tags/{}", name);
        self.repo
            .reference(&ref_name, tag_oid, true, "create tag")
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(TagInfo {
            name: name.to_string(),
            oid: CommitOid::from_git2(tag_oid),
            target: target.clone(),
            is_annotated: true,
            message: Some(message.to_string()),
            tagger: Some(tagger.name.clone()),
            tagger_email: Some(tagger.email.clone()),
        })
    }

    pub fn tag_create_lightweight(
        &self,
        name: &str,
        target: &CommitOid,
        force: bool,
    ) -> GitResult<TagInfo> {
        validate_ref_name(name)?;

        let target_oid = target
            .to_oid()
            .map_err(|_| GitError::InvalidOid(target.to_string()))?;

        let obj = self
            .repo()
            .find_object(target_oid, None)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let tag_oid = self
            .repo()
            .tag_lightweight(name, &obj, force)
            .map_err(|e| {
                if e.code() == git2::ErrorCode::Exists {
                    GitError::TagExists(name.to_string())
                } else {
                    GitError::Internal(e.to_string())
                }
            })?;

        Ok(TagInfo {
            name: name.to_string(),
            oid: CommitOid::from_git2(tag_oid),
            target: target.clone(),
            is_annotated: false,
            message: None,
            tagger: None,
            tagger_email: None,
        })
    }

    pub fn tag_delete(&self, name: &str) -> GitResult<()> {
        let full_name = if name.starts_with("refs/tags/") {
            name.to_string()
        } else {
            format!("refs/tags/{}", name)
        };

        let mut reference = self
            .repo()
            .find_reference(&full_name)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        reference
            .delete()
            .map_err(|e| GitError::Internal(e.to_string()))
    }

    pub fn tag_rename(&self, old_name: &str, new_name: &str) -> GitResult<TagInfo> {
        validate_ref_name(new_name)?;

        let old_ref = if old_name.starts_with("refs/tags/") {
            old_name.to_string()
        } else {
            format!("refs/tags/{}", old_name)
        };

        let new_ref = format!("refs/tags/{}", new_name);

        let info = self.tag_get(old_name)?;

        let mut reference = self
            .repo()
            .find_reference(&old_ref)
            .map_err(|_e| GitError::RefNotFound(old_name.to_string()))?;

        let target_oid = reference
            .target()
            .ok_or_else(|| GitError::Internal("tag has no target".to_string()))?;

        reference
            .delete()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        self.repo
            .reference(&new_ref, target_oid, true, "rename tag")
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(TagInfo {
            name: new_name.to_string(),
            oid: info.oid,
            target: info.target,
            is_annotated: info.is_annotated,
            message: info.message,
            tagger: info.tagger,
            tagger_email: info.tagger_email,
        })
    }

    pub fn tag_update_message(
        &self,
        name: &str,
        message: &str,
        tagger: &CommitSignature,
    ) -> GitResult<TagInfo> {
        let full_name = if name.starts_with("refs/tags/") {
            name.to_string()
        } else {
            format!("refs/tags/{}", name)
        };

        let reference = self
            .repo()
            .find_reference(&full_name)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        let tag_oid = reference
            .target()
            .ok_or_else(|| GitError::Internal("tag reference has no target".to_string()))?;

        let tag_obj = self
            .repo()
            .find_object(tag_oid, None)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        if tag_obj.kind() != Some(git2::ObjectType::Tag) {
            return Err(GitError::Internal(
                "cannot update message of a lightweight tag".to_string(),
            ));
        }

        let commit_obj = tag_obj
            .as_tag()
            .and_then(|t| t.peel().ok())
            .ok_or_else(|| GitError::Internal("cannot peel tag to commit".to_string()))?;

        let sig = self.commit_signature_to_git2(tagger)?;

        let new_tag_oid = self
            .repo()
            .tag(name, &commit_obj, &sig, message, true)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let temp_name = format!("{}/update-tmp-{}", full_name, std::process::id());
        self.repo
            .reference(&temp_name, new_tag_oid, true, "update tag message (temp)")
            .map_err(|e| GitError::Internal(e.to_string()))?;

        self.repo
            .reference(&full_name, new_tag_oid, true, "update tag message")
            .map_err(|e| GitError::Internal(e.to_string()))?;

        if let Ok(mut temp_ref) = self.repo().find_reference(&temp_name) {
            if let Err(e) = temp_ref.delete() {
                // Log but do not fail — a leftover temporary reference is non-critical
                // but indicates something went wrong during tag update.
                eprintln!(
                    "failed to delete temporary tag reference {}: {}",
                    temp_name, e
                );
            }
        }

        Ok(TagInfo {
            name: name.to_string(),
            oid: CommitOid::from_git2(new_tag_oid),
            target: CommitOid::from_git2(commit_obj.id()),
            is_annotated: true,
            message: Some(message.to_string()),
            tagger: Some(tagger.name.clone()),
            tagger_email: Some(tagger.email.clone()),
        })
    }
}
