//! Branch merge operations.

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

impl GitDomain {
    pub fn branch_is_merged(&self, branch: &str, into: &str) -> GitResult<bool> {
        let branch_oid = self
            .branch_target(branch)?
            .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", branch)))?
            .to_oid()?;
        let into_oid = self
            .branch_target(into)?
            .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", into)))?
            .to_oid()?;

        self.repo
            .graph_ahead_behind(into_oid, branch_oid)
            .map(|(ahead, _)| ahead == 0)
            .map_err(|e| GitError::Internal(e.to_string()))
    }

    pub fn branch_merge_base(&self, branch1: &str, branch2: &str) -> GitResult<CommitOid> {
        let oid1 = self
            .branch_target(branch1)?
            .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", branch1)))?
            .to_oid()?;
        let oid2 = self
            .branch_target(branch2)?
            .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", branch2)))?
            .to_oid()?;

        let base = self
            .repo()
            .merge_base(oid1, oid2)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(CommitOid::from_git2(base))
    }

    pub fn branch_is_ancestor(&self, child: &str, ancestor: &str) -> GitResult<bool> {
        let child_oid = self
            .branch_target(child)?
            .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", child)))?
            .to_oid()?;
        let ancestor_oid = self
            .branch_target(ancestor)?
            .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", ancestor)))?
            .to_oid()?;

        self.repo
            .graph_ahead_behind(child_oid, ancestor_oid)
            .map(|(_, behind)| behind == 0)
            .map_err(|e| GitError::Internal(e.to_string()))
    }

    pub fn branch_fast_forward(&self, target: &str, force: bool) -> GitResult<CommitOid> {
        let target_oid = self
            .branch_target(target)?
            .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", target)))?
            .to_oid()?;

        let head = self
            .repo()
            .head()
            .ok()
            .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?;
        let head_oid = head
            .target()
            .ok_or_else(|| GitError::Internal("HEAD has no target".to_string()))?;
        let head_name = head
            .name()
            .ok_or_else(|| GitError::Internal("HEAD has no name".to_string()))?;

        let ref_name = if head_name.starts_with("refs/") {
            head_name.to_string()
        } else {
            format!("refs/heads/{}", head_name)
        };

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

        if behind == 0 {
            return Ok(CommitOid::from_git2(head_oid));
        }

        if !force && ahead > 0 {
            // ahead > 0 && behind > 0 means diverged; ahead == 0 && behind > 0 is valid FF
            return Err(GitError::Internal(
                "not a fast-forward: branches have diverged".to_string(),
            ));
        }

        self.repo
            .reference_matching(
                ref_name.as_str(),
                target_oid,
                true,
                head_oid,
                "fast-forward",
            )
            .map_err(|e| GitError::Internal(e.to_string()))?;

        self.repo
            .set_head_detached(target_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        self.repo
            .checkout_tree(self.repo.find_commit(target_oid)?.as_object(), None)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(CommitOid::from_git2(target_oid))
    }

    pub fn branch_abort_merge(&self) -> GitResult<()> {
        let head_oid = self
            .repo()
            .head()
            .ok()
            .and_then(|r| r.target())
            .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?;

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

        self.repo
            .reset(&obj, git2::ResetType::Hard, None)
            .map_err(|e| GitError::Internal(e.to_string()))
    }

    pub fn branch_is_conflicted(&self) -> bool {
        self.repo
            .index()
            .map(|idx| idx.has_conflicts())
            .unwrap_or(false)
    }

    pub fn branch_tracking_difference(&self, name: &str) -> GitResult<(usize, usize)> {
        let upstream = self.branch_upstream(name)?;
        match upstream {
            Some(u) => self.branch_ahead_behind(name, &u.name),
            None => Ok((0, 0)),
        }
    }
}
