//! Merge operations.

use crate::commit::types::CommitOid;
use crate::merge::types::{
    MergeAnalysisResult, MergeOptions, MergePreferenceResult, MergeheadInfo,
};
use crate::{GitDomain, GitError, GitResult};

impl GitDomain {
    pub fn merge_analysis(
        &self,
        their_oid: &CommitOid,
    ) -> GitResult<(MergeAnalysisResult, MergePreferenceResult)> {
        let oid = their_oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(their_oid.to_string()))?;

        let head_ref = self
            .repo()
            .find_reference("HEAD")
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let annotated = self
            .repo()
            .reference_to_annotated_commit(&head_ref)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let their_annotated = self
            .repo()
            .find_annotated_commit(oid)
            .map_err(|e: git2::Error| GitError::Internal(e.to_string()))?;

        let (analysis, pref) = self
            .repo()
            .merge_analysis(&[&annotated, &their_annotated])
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok((
            MergeAnalysisResult::from_git2(analysis),
            MergePreferenceResult::from_git2(pref),
        ))
    }

    pub fn merge_analysis_for_ref(
        &self,
        ref_name: &str,
        their_oid: &CommitOid,
    ) -> GitResult<(MergeAnalysisResult, MergePreferenceResult)> {
        let oid = their_oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(their_oid.to_string()))?;

        let reference = self
            .repo()
            .find_reference(ref_name)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let their_annotated = self
            .repo()
            .find_annotated_commit(oid)
            .map_err(|e: git2::Error| GitError::Internal(e.to_string()))?;

        let (analysis, pref) = self
            .repo()
            .merge_analysis_for_ref(&reference, &[&their_annotated])
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok((
            MergeAnalysisResult::from_git2(analysis),
            MergePreferenceResult::from_git2(pref),
        ))
    }

    pub fn merge_base(&self, oid1: &CommitOid, oid2: &CommitOid) -> GitResult<CommitOid> {
        let o1 = oid1
            .to_oid()
            .map_err(|_| GitError::InvalidOid(oid1.to_string()))?;
        let o2 = oid2
            .to_oid()
            .map_err(|_| GitError::InvalidOid(oid2.to_string()))?;

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

        Ok(CommitOid::from_git2(base))
    }

    pub fn merge_base_many(&self, oids: &[CommitOid]) -> GitResult<CommitOid> {
        let oids: Vec<_> = oids
            .iter()
            .map(|o| o.to_oid().map_err(|_| GitError::InvalidOid(o.to_string())))
            .collect::<Result<Vec<_>, _>>()?;

        let base = self
            .repo()
            .merge_base_many(&oids)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(CommitOid::from_git2(base))
    }

    pub fn merge_base_octopus(&self, oids: &[CommitOid]) -> GitResult<CommitOid> {
        let oids: Vec<_> = oids
            .iter()
            .map(|o| o.to_oid().map_err(|_| GitError::InvalidOid(o.to_string())))
            .collect::<Result<Vec<_>, _>>()?;

        let base = self
            .repo()
            .merge_base_octopus(&oids)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(CommitOid::from_git2(base))
    }

    pub fn merge_commits(
        &self,
        local_commit: &CommitOid,
        remote_commit: &CommitOid,
        opts: Option<MergeOptions>,
    ) -> GitResult<()> {
        let local_oid = local_commit
            .to_oid()
            .map_err(|_| GitError::InvalidOid(local_commit.to_string()))?;
        let remote_oid = remote_commit
            .to_oid()
            .map_err(|_| GitError::InvalidOid(remote_commit.to_string()))?;

        let local = self
            .repo()
            .find_commit(local_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let remote = self
            .repo()
            .find_commit(remote_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let mut merge_opts = opts
            .map(|o| o.to_git2())
            .unwrap_or_else(git2::MergeOptions::new);

        self.repo()
            .merge_commits(&local, &remote, Some(&mut merge_opts))
            .map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(())
    }

    pub fn merge_trees(
        &self,
        ancestor_tree: &CommitOid,
        our_tree: &CommitOid,
        their_tree: &CommitOid,
        opts: Option<MergeOptions>,
    ) -> GitResult<()> {
        let ancestor_oid = ancestor_tree
            .to_oid()
            .map_err(|_| GitError::InvalidOid(ancestor_tree.to_string()))?;
        let our_oid = our_tree
            .to_oid()
            .map_err(|_| GitError::InvalidOid(our_tree.to_string()))?;
        let their_oid = their_tree
            .to_oid()
            .map_err(|_| GitError::InvalidOid(their_tree.to_string()))?;

        let ancestor = self
            .repo()
            .find_tree(ancestor_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let ours = self
            .repo()
            .find_tree(our_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let theirs = self
            .repo()
            .find_tree(their_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let mut merge_opts = opts
            .map(|o| o.to_git2())
            .unwrap_or_else(git2::MergeOptions::new);

        self.repo()
            .merge_trees(&ancestor, &ours, &theirs, Some(&mut merge_opts))
            .map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(())
    }

    pub fn merge_abort(&self) -> GitResult<()> {
        self.repo()
            .cleanup_state()
            .map_err(|e| GitError::Internal(e.to_string()))
    }

    pub fn merge_is_in_progress(&self) -> bool {
        matches!(
            self.repo().state(),
            git2::RepositoryState::Merge
                | git2::RepositoryState::Revert
                | git2::RepositoryState::RevertSequence
                | git2::RepositoryState::CherryPick
                | git2::RepositoryState::CherryPickSequence
        )
    }

    pub fn mergehead_list(&mut self) -> GitResult<Vec<MergeheadInfo>> {
        let mut heads = Vec::new();
        self.repo_mut()?
            .mergehead_foreach(|oid| {
                heads.push(MergeheadInfo {
                    oid: CommitOid::from_git2(*oid),
                });
                true
            })
            .map_err(|e: git2::Error| GitError::Internal(e.to_string()))?;
        Ok(heads)
    }

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

    /// Squash all commits from `source_branch` into a single commit on top of `base`.
    pub fn squash_commits(&self, base: &CommitOid, source_branch: &str) -> GitResult<CommitOid> {
        let base_oid = base
            .to_oid()
            .map_err(|_| GitError::InvalidOid(base.to_string()))?;

        let source_ref = self
            .repo()
            .find_reference(source_branch)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let head_oid = source_ref
            .target()
            .ok_or_else(|| GitError::Internal("Branch has no target OID".to_string()))?;

        // Get the merge base (the common ancestor)
        let merge_base = self
            .repo()
            .merge_base(base_oid, head_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        // Collect all commits from merge_base (exclusive) to head (inclusive)
        let mut revwalk = self
            .repo()
            .revwalk()
            .map_err(|e| GitError::Internal(e.to_string()))?;
        revwalk
            .push(head_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        revwalk
            .hide(merge_base)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let mut commits: Vec<git2::Commit> = Vec::new();
        for oid_result in revwalk {
            let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?;
            let commit = self
                .repo()
                .find_commit(oid)
                .map_err(|e| GitError::Internal(e.to_string()))?;
            commits.push(commit);
        }

        if commits.is_empty() {
            // Nothing to squash — return base as-is
            return Ok(CommitOid::from_git2(base_oid));
        }

        // Sort commits oldest-first (topological order)
        commits.sort_by_key(|c| c.time().seconds());

        // Apply all patches onto a temporary tree.
        // Strategy: apply patches sequentially using `git2::apply` on the index.
        let base_tree = self
            .repo()
            .find_commit(base_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?
            .tree()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        // Build a diff from the accumulated patches
        let sig = self
            .repo()
            .signature()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        // Apply each commit's diff sequentially to build the squash tree.
        let mut current_tree = base_tree;
        for commit in &commits {
            let commit_tree = commit
                .tree()
                .map_err(|e| GitError::Internal(e.to_string()))?;
            let diff = self
                .repo()
                .diff_tree_to_tree(Some(&current_tree), Some(&commit_tree), None)
                .map_err(|e| GitError::Internal(e.to_string()))?;

            // apply_to_tree applies the diff to current_tree, returning a new Index.
            let mut new_index = self
                .repo()
                .apply_to_tree(&current_tree, &diff, None)
                .map_err(|e| GitError::Internal(e.to_string()))?;

            let new_tree_oid = new_index
                .write_tree()
                .map_err(|e| GitError::Internal(e.to_string()))?;

            current_tree = self
                .repo()
                .find_tree(new_tree_oid)
                .map_err(|e| GitError::Internal(e.to_string()))?;
        }

        let squash_tree = current_tree;

        // Build the squash commit message: list all squashed commits
        let mut msg = String::new();
        for commit in &commits {
            if !msg.is_empty() {
                msg.push_str("\n");
            }
            msg.push_str(&format!("- {}", commit.summary().unwrap_or("(no message)")));
        }

        // Create the squash commit on top of base
        let squash_oid = self
            .repo()
            .commit(
                Some("HEAD"),
                &sig,
                &sig,
                &msg,
                &squash_tree,
                &[&self
                    .repo()
                    .find_commit(base_oid)
                    .map_err(|e| GitError::Internal(e.to_string()))?],
            )
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(CommitOid::from_git2(squash_oid))
    }
}
