//! Rebase operations.
//!
//! Manual rebase implementation since git2's `RebaseSession` has type inference
//! issues with `Option<&[AnnotatedCommit]>` in Rust. The approach:
//!  1. Walk all commits from `base_oid` (exclusive) to `source_oid` (inclusive).
//!  2. For each commit, apply its tree diff onto the current HEAD.
//!  3. Create a new commit with the same message on top of the current HEAD.

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

impl GitDomain {
    /// Rebase the commits on `source_oid` onto `base_oid`.
    pub fn rebase_commits(
        &self,
        base_oid: &CommitOid,
        source_oid: &CommitOid,
    ) -> GitResult<CommitOid> {
        let base = base_oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(base_oid.to_string()))?;
        let source = source_oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(source_oid.to_string()))?;

        // Collect all commits from base (exclusive) to source (inclusive).
        let mut revwalk = self
            .repo()
            .revwalk()
            .map_err(|e| GitError::Internal(e.to_string()))?;
        revwalk
            .push(source)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        revwalk
            .hide(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() {
            return Err(GitError::Internal("No commits to rebase".to_string()));
        }

        // Sort oldest-first (topological). Use OID as tiebreaker for commits with identical timestamps.
        commits.sort_by(|a, b| {
            let time_cmp = a.time().seconds().cmp(&b.time().seconds());
            if time_cmp != std::cmp::Ordering::Equal {
                time_cmp
            } else {
                // Same timestamp: use OID for deterministic ordering.
                a.id().cmp(&b.id())
            }
        });

        let sig = self
            .repo()
            .signature()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        // Start with the base commit's tree.
        let base_commit = self
            .repo()
            .find_commit(base)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let mut current_tree = base_commit
            .tree()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let mut last_oid = base;

        for commit in &commits {
            let parent_tree = &current_tree;
            let commit_tree = commit
                .tree()
                .map_err(|e| GitError::Internal(e.to_string()))?;

            // Diff the parent tree with this commit's tree.
            let diff = self
                .repo()
                .diff_tree_to_tree(Some(parent_tree), Some(&commit_tree), None)
                .map_err(|e| GitError::Internal(e.to_string()))?;

            // Apply the diff to parent_tree, producing a new tree.
            let mut new_index = self
                .repo()
                .apply_to_tree(parent_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()))?;

            // Find the parent commit for the rebased commit.
            let parent_commit = self
                .repo()
                .find_commit(last_oid)
                .map_err(|e| GitError::Internal(e.to_string()))?;

            // Create the rebased commit on top of the current base.
            let new_oid = self
                .repo()
                .commit(
                    Some("HEAD"),
                    &sig,
                    &sig,
                    commit.message().unwrap_or(""),
                    &current_tree,
                    &[&parent_commit],
                )
                .map_err(|e| GitError::Internal(e.to_string()))?;

            last_oid = new_oid;
        }

        Ok(CommitOid::from_git2(last_oid))
    }

    pub fn rebase_abort(&self) -> GitResult<()> {
        // git2 rebase sessions are not persistent across process exits.
        // The caller resets HEAD to the original position.
        Ok(())
    }
}
