//! Tag querying operations.

use crate::commit::types::CommitOid;
use crate::tags::types::{TagInfo, TagSummary};
use crate::{GitDomain, GitError, GitResult};

impl GitDomain {
    pub fn tag_list(&self) -> GitResult<Vec<TagInfo>> {
        let tag_names = self
            .repo()
            .tag_names(None)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let mut tags = Vec::with_capacity(16);
        let mut errors: Vec<(String, GitError)> = Vec::new();
        let count = tag_names.len();
        for i in 0..count {
            if let Some(name) = tag_names.get(i) {
                match self.tag_get(name) {
                    Ok(info) => tags.push(info),
                    Err(e) => errors.push((name.to_string(), e)),
                }
            }
        }
        if !errors.is_empty() {
            return Err(GitError::Internal(format!(
                "failed to get {} tag(s): {}",
                errors.len(),
                errors
                    .into_iter()
                    .map(|(n, e)| format!("{}: {}", n, e))
                    .collect::<Vec<_>>()
                    .join("; ")
            )));
        }
        Ok(tags)
    }

    pub fn tag_list_names(&self) -> GitResult<Vec<String>> {
        let names = self
            .repo()
            .tag_names(None)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let count = names.len();
        let mut result = Vec::with_capacity(count);
        for i in 0..count {
            if let Some(name) = names.get(i) {
                result.push(name.to_string());
            }
        }
        Ok(result)
    }

    pub fn tag_count(&self) -> GitResult<usize> {
        let names = self.tag_list_names()?;
        Ok(names.len())
    }

    pub fn tag_summary(&self) -> GitResult<TagSummary> {
        let count = self.tag_count()?;
        Ok(TagSummary { total_count: count })
    }

    pub fn tag_get(&self, name: &str) -> 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 target_oid = reference
            .target()
            .ok_or_else(|| GitError::Internal("tag reference has no target".to_string()))?;

        let target = CommitOid::from_git2(target_oid);

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

        if obj.kind() == Some(git2::ObjectType::Tag) {
            let tag = self
                .repo()
                .find_tag(target_oid)
                .map_err(|e| GitError::Internal(e.to_string()))?;

            let tagger = tag.tagger().map(|s| {
                (
                    s.name().unwrap_or("").to_string(),
                    s.email().unwrap_or("").to_string(),
                )
            });

            Ok(TagInfo {
                name: name.to_string(),
                oid: CommitOid::from_git2(target_oid),
                target,
                is_annotated: true,
                message: tag.message().map(String::from),
                tagger: tagger.as_ref().map(|(n, _)| n.clone()),
                tagger_email: tagger.as_ref().map(|(_, e)| e.clone()),
            })
        } else {
            Ok(TagInfo {
                name: name.to_string(),
                oid: CommitOid::from_git2(target_oid),
                target,
                is_annotated: false,
                message: None,
                tagger: None,
                tagger_email: None,
            })
        }
    }

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

        self.repo.find_reference(&full_name).is_ok()
    }

    pub fn tag_target(&self, name: &str) -> GitResult<Option<CommitOid>> {
        let info = self.tag_get(name)?;
        Ok(Some(info.target))
    }

    pub fn tag_is_annotated(&self, name: &str) -> GitResult<bool> {
        let (_, is_tag) = self.tag_reference_info(name)?;
        Ok(is_tag)
    }

    pub fn tag_message(&self, name: &str) -> GitResult<Option<String>> {
        if let Some(tag) = self.find_annotated_tag(name)? {
            Ok(tag.message().map(String::from))
        } else {
            Ok(None)
        }
    }

    pub fn tag_tagger(&self, name: &str) -> GitResult<Option<(String, String)>> {
        if let Some(tag) = self.find_annotated_tag(name)? {
            Ok(tag.tagger().map(|s| {
                (
                    s.name().unwrap_or("").to_string(),
                    s.email().unwrap_or("").to_string(),
                )
            }))
        } else {
            Ok(None)
        }
    }

    /// Look up a tag's reference OID and whether it is an annotated tag.
    fn tag_reference_info(&self, name: &str) -> GitResult<(git2::Oid, bool)> {
        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 target_oid = reference
            .target()
            .ok_or_else(|| GitError::Internal("tag reference has no target".to_string()))?;

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

        let is_tag = obj.kind() == Some(git2::ObjectType::Tag);
        Ok((target_oid, is_tag))
    }

    /// Find the git2 Tag object for an annotated tag, or None if it is a lightweight tag.
    fn find_annotated_tag(&self, name: &str) -> GitResult<Option<git2::Tag<'_>>> {
        let (target_oid, is_tag) = self.tag_reference_info(name)?;
        if !is_tag {
            return Ok(None);
        }
        let tag = self
            .repo()
            .find_tag(target_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(Some(tag))
    }
}
