//! Commit reference (branch/tag) and reflog operations.

use std::collections::HashMap;

use crate::commit::types::*;
use crate::{GitDomain, GitError, GitResult};

impl GitDomain {
    pub fn commit_refs(&self, oid: &CommitOid) -> GitResult<Vec<CommitRefInfo>> {
        let _ = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;

        let mut refs = Vec::new();

        for branch_result in self
            .repo()
            .branches(Some(git2::BranchType::Local))
            .map_err(|e| GitError::Internal(e.to_string()))?
        {
            let (branch, _) = branch_result.map_err(|e| GitError::Internal(e.to_string()))?;

            if let Some(target) = branch.get().target() {
                if target == oid.to_oid()? {
                    let name = branch
                        .name()
                        .map_err(|e| GitError::Internal(e.to_string()))?
                        .unwrap_or("")
                        .to_string();
                    refs.push(CommitRefInfo {
                        name,
                        target: oid.clone(),
                        is_remote: false,
                        is_tag: false,
                    });
                }
            }
        }

        if let Ok(walk) = self.repo.references() {
            for ref_result in walk {
                if let Ok(r) = ref_result {
                    if let Some(name) = r.name() {
                        if name.starts_with("refs/tags/") {
                            if let Some(target) = r.target() {
                                if target == oid.to_oid()? {
                                    refs.push(CommitRefInfo {
                                        name: name.to_string(),
                                        target: oid.clone(),
                                        is_remote: false,
                                        is_tag: true,
                                    });
                                }
                            }
                        }
                    }
                }
            }
        }

        Ok(refs)
    }

    pub fn commit_branches(&self, oid: &CommitOid) -> GitResult<Vec<String>> {
        let refs = self.commit_refs(oid)?;
        Ok(refs
            .into_iter()
            .filter(|r| !r.is_remote && !r.is_tag)
            .map(|r| r.name)
            .collect())
    }

    pub fn commit_tags(&self, oid: &CommitOid) -> GitResult<Vec<String>> {
        let refs = self.commit_refs(oid)?;
        Ok(refs
            .into_iter()
            .filter(|r| r.is_tag)
            .map(|r| r.name)
            .collect())
    }

    pub fn commit_is_tip(&self, oid: &CommitOid) -> GitResult<bool> {
        let refs = self.commit_refs(oid)?;
        Ok(!refs.is_empty())
    }

    pub fn commit_is_default_tip(&self, oid: &CommitOid) -> GitResult<bool> {
        let default = self.repo.head().ok().and_then(|r| r.target());
        Ok(default == Some(oid.to_oid()?))
    }

    pub fn commit_reflog(
        &self,
        oid: &CommitOid,
        refname: Option<&str>,
    ) -> GitResult<Vec<CommitReflogEntry>> {
        let refname = match refname {
            Some(name) => name.to_string(),
            None => self
                .repo()
                .head()
                .ok()
                .and_then(|r| r.name().map(|n| n.to_owned()))
                .ok_or_else(|| GitError::Internal("HEAD has no name".to_string()))?,
        };

        let reflog = self
            .repo()
            .reflog(&refname)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let mut entries = Vec::new();
        let oid_val = oid.to_oid()?;
        for entry in reflog.iter() {
            if entry.id_new() == oid_val || entry.id_old() == oid_val {
                let sig = entry.committer();
                entries.push(CommitReflogEntry {
                    oid_new: CommitOid::from_git2(entry.id_new()),
                    oid_old: CommitOid::from_git2(entry.id_old()),
                    committer_name: sig.name().unwrap_or("").to_string(),
                    committer_email: sig.email().unwrap_or("").to_string(),
                    time_secs: sig.when().seconds(),
                    message: entry.message().map(String::from),
                    ref_name: refname.clone(),
                });
            }
        }

        Ok(entries)
    }

    pub fn commit_ref_count(&self, oid: &CommitOid) -> GitResult<usize> {
        let refs = self.commit_refs(oid)?;
        Ok(refs.len())
    }

    /// Returns all refs (branches + tags) grouped by commit OID.
    pub fn refs_grouped(&self) -> GitResult<HashMap<String, (Vec<String>, Vec<String>)>> {
        let mut map: HashMap<String, (Vec<String>, Vec<String>)> = HashMap::new();

        for branch_result in self
            .repo()
            .branches(Some(git2::BranchType::Local))
            .map_err(|e| GitError::Internal(e.to_string()))?
        {
            let (branch, _) = branch_result.map_err(|e| GitError::Internal(e.to_string()))?;
            if let Some(target) = branch.get().target() {
                let oid_str = target.to_string();
                let name = branch
                    .name()
                    .map_err(|e| GitError::Internal(e.to_string()))?
                    .unwrap_or("")
                    .to_string();
                let entry = map
                    .entry(oid_str)
                    .or_insert_with(|| (Vec::new(), Vec::new()));
                entry.0.push(name);
            }
        }

        if let Ok(walk) = self.repo.references() {
            for ref_result in walk {
                if let Ok(r) = ref_result {
                    if let Some(name) = r.name() {
                        if name.starts_with("refs/tags/") {
                            if let Some(target) = r.target() {
                                let oid_str = target.to_string();
                                let full_name = name.to_string();
                                let entry = map
                                    .entry(oid_str)
                                    .or_insert_with(|| (Vec::new(), Vec::new()));
                                entry.1.push(full_name);
                            }
                        }
                    }
                }
            }
        }

        Ok(map)
    }

    /// Returns all reflog entries for a given ref (defaults to HEAD).
    pub fn reflog_entries(&self, refname: Option<&str>) -> GitResult<Vec<CommitReflogEntry>> {
        let refname = match refname {
            Some(name) => name.to_string(),
            None => self
                .repo()
                .head()
                .ok()
                .and_then(|r| r.name().map(|n| n.to_owned()))
                .ok_or_else(|| GitError::Internal("HEAD has no name".to_string()))?,
        };

        let reflog = self
            .repo()
            .reflog(&refname)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let mut entries = Vec::new();
        for entry in reflog.iter() {
            let sig = entry.committer();
            entries.push(CommitReflogEntry {
                oid_new: CommitOid::from_git2(entry.id_new()),
                oid_old: CommitOid::from_git2(entry.id_old()),
                committer_name: sig.name().unwrap_or("").to_string(),
                committer_email: sig.email().unwrap_or("").to_string(),
                time_secs: sig.when().seconds(),
                message: entry.message().map(String::from),
                ref_name: refname.clone(),
            });
        }

        Ok(entries)
    }
}
