//! Branch querying operations.

use git2::BranchType;

use crate::branch::types::{BranchDiff, BranchInfo, BranchSummary};
use crate::commit::types::CommitOid;
use crate::{GitDomain, GitError, GitResult};

impl GitDomain {
    pub fn branch_list(&self, remote_only: bool) -> GitResult<Vec<BranchInfo>> {
        let branch_type = if remote_only {
            BranchType::Remote
        } else {
            BranchType::Local
        };

        let mut branches = Vec::with_capacity(16);
        // Keep head_name as full ref for comparison with branch names
        let head_name = self.repo.head().ok().and_then(|r| r.name().map(String::from));

        for branch_result in self
            .repo()
            .branches(Some(branch_type))
            .map_err(|e| GitError::Internal(e.to_string()))?
        {
            let (branch, _) = branch_result.map_err(|e| GitError::Internal(e.to_string()))?;
            if let Some(name) = branch.name().ok().flatten() {
                let Some(target) = branch.get().target() else {
                    continue; // skip branches without a target
                };
                let name = name.to_string();
                let oid = CommitOid::from_git2(target);
                // Compare full ref names (e.g., "refs/heads/main" == "refs/heads/main")
                let is_head = head_name.as_ref().map_or(false, |h| h == &name);
                let is_current = branch.is_head();

                branches.push(BranchInfo {
                    name,
                    oid,
                    is_head,
                    is_remote: remote_only,
                    is_current,
                    upstream: None,
                });
            }
        }

        Ok(branches)
    }

    pub fn branch_list_local(&self) -> GitResult<Vec<BranchInfo>> {
        self.branch_list(false)
    }

    pub fn branch_list_remote(&self) -> GitResult<Vec<BranchInfo>> {
        self.branch_list(true)
    }

    pub fn branch_list_all(&self) -> GitResult<Vec<BranchInfo>> {
        let mut all = self.branch_list(false)?;
        let remote = self.branch_list(true)?;
        for mut r in remote {
            r.is_remote = true;
            all.push(r);
        }
        Ok(all)
    }

    pub fn branch_summary(&self) -> GitResult<BranchSummary> {
        let local = self.branch_list(false)?;
        let remote = self.branch_list(true)?;
        Ok(BranchSummary {
            local_count: local.len(),
            remote_count: remote.len(),
            all_count: local.len() + remote.len(),
        })
    }

    pub fn branch_get(&self, name: &str) -> GitResult<BranchInfo> {
        // Determine candidates: try full refs as-is, then construct refs/heads/ and refs/remotes/
        let candidates: Vec<String> = if name.starts_with("refs/") {
            vec![name.to_string()]
        } else {
            vec![
                format!("refs/heads/{}", name),
                format!("refs/remotes/{}", name),
            ]
        };

        // Try local branch first, then remote
        let branch = candidates
            .iter()
            .find_map(|full_name| {
                self.repo
                    .find_branch(full_name, git2::BranchType::Local)
                    .ok()
                    .or_else(|| self.repo.find_branch(full_name, git2::BranchType::Remote).ok())
            })
            .ok_or_else(|| GitError::RefNotFound(name.to_string()))?;

        let full_name = branch.name().ok().flatten().unwrap_or_default().to_string();
        let target = branch
            .get()
            .target()
            .ok_or_else(|| GitError::Internal("branch has no target".to_string()))?;
        let oid = CommitOid::from_git2(target);
        let head_name = self
            .repo()
            .head()
            .ok()
            .and_then(|r| r.name().map(String::from));
        let branch_name = branch.name().ok().flatten().unwrap_or_default();

        Ok(BranchInfo {
            name: branch_name.to_string(),
            oid,
            is_head: head_name.as_ref().map_or(false, |h| h == &full_name),
            is_remote: full_name.starts_with("refs/remotes/"),
            is_current: branch.is_head(),
            upstream: branch.upstream().ok().map(|u| {
                u.name()
                    .ok()
                    .and_then(|n| n)
                    .unwrap_or_default()
                    .to_string()
            }),
        })
    }

    pub fn branch_exists(&self, name: &str) -> bool {
        // Same candidate logic as branch_get
        let candidates: Vec<String> = if name.starts_with("refs/") {
            vec![name.to_string()]
        } else {
            vec![
                format!("refs/heads/{}", name),
                format!("refs/remotes/{}", name),
            ]
        };

        candidates.iter().any(|full_name| {
            self.repo
                .find_branch(full_name, BranchType::Local)
                .is_ok()
                || self
                    .repo()
                    .find_branch(full_name, BranchType::Remote)
                    .is_ok()
        })
    }

    pub fn branch_is_head(&self, name: &str) -> GitResult<bool> {
        let info = self.branch_get(name)?;
        Ok(info.is_head)
    }

    pub fn branch_current(&self) -> GitResult<Option<BranchInfo>> {
        let head = self
            .repo()
            .head()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        if let Some(name) = head.name() {
            let name = name.to_string();
            if name.starts_with("refs/heads/") {
                return Ok(Some(self.branch_get(&name)?));
            }
        }

        Ok(None)
    }

    pub fn branch_target(&self, name: &str) -> GitResult<Option<CommitOid>> {
        let info = self.branch_get(name)?;
        if info.oid.0.is_empty() {
            Ok(None)
        } else {
            Ok(Some(info.oid))
        }
    }

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

        let branch = self
            .repo()
            .find_branch(&full_name, BranchType::Local)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        match branch.upstream() {
            Ok(up) => {
                let up_target = up.get().target().ok_or_else(|| {
                    GitError::Internal("upstream branch has no target".to_string())
                })?;
                Ok(Some(BranchInfo {
                    name: up
                        .name()
                        .ok()
                        .and_then(|n| n)
                        .unwrap_or_default()
                        .to_string(),
                    oid: CommitOid::from_git2(up_target),
                    is_head: false,
                    is_remote: true,
                    is_current: false,
                    upstream: None,
                }))
            }
            Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(None),
            Err(e) => Err(GitError::Internal(e.to_string())),
        }
    }

    pub fn branch_upstream_name(&self, name: &str) -> GitResult<Option<String>> {
        let upstream = self.branch_upstream(name)?;
        Ok(upstream.map(|u| u.name))
    }

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

        let branch = self
            .repo()
            .find_branch(&full_name, BranchType::Local)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        Ok(branch.upstream().is_ok())
    }

    pub fn branch_is_detached(&self) -> bool {
        self.repo.head().map_or(false, |h| !h.is_branch())
    }

    pub fn branch_diff(&self, local: &str, remote: &str) -> GitResult<BranchDiff> {
        let local_oid = self.branch_target(local)?;
        let remote_oid = self.branch_target(remote)?;

        match (local_oid, remote_oid) {
            (Some(l), Some(r)) => {
                let l_oid = l.to_oid()?;
                let r_oid = r.to_oid()?;

                let (ahead, behind) = self
                    .repo()
                    .graph_ahead_behind(l_oid, r_oid)
                    .map_err(|e| GitError::Internal(e.to_string()))?;

                Ok(BranchDiff {
                    ahead,
                    behind,
                    diverged: ahead > 0 && behind > 0,
                })
            }
            _ => Ok(BranchDiff {
                ahead: 0,
                behind: 0,
                diverged: false,
            }),
        }
    }

    pub fn branch_ahead_behind(&self, local: &str, upstream: &str) -> GitResult<(usize, usize)> {
        let diff = self.branch_diff(local, upstream)?;
        Ok((diff.ahead, diff.behind))
    }
}
