//! Diff operations.

use std::cell::RefCell;

use crate::commit::types::CommitOid;
use crate::diff::types::{
    DiffDelta, DiffHunk, DiffLine, DiffOptions, DiffResult, DiffStats, SideBySideChangeType,
    SideBySideDiffResult, SideBySideFile, SideBySideLine,
};
use crate::{GitDomain, GitError, GitResult};

impl GitDomain {
    pub fn diff_tree_to_tree(
        &self,
        old_tree: Option<&CommitOid>,
        new_tree: Option<&CommitOid>,
        opts: Option<DiffOptions>,
    ) -> GitResult<DiffResult> {
        let old_tree = match old_tree {
            Some(oid) => {
                let o = oid
                    .to_oid()
                    .map_err(|_| GitError::InvalidOid(oid.to_string()))?;
                Some(
                    self.repo()
                        .find_tree(o)
                        .map_err(|e| GitError::Internal(e.to_string()))?,
                )
            }
            None => None,
        };
        let new_tree = match new_tree {
            Some(oid) => {
                let o = oid
                    .to_oid()
                    .map_err(|_| GitError::InvalidOid(oid.to_string()))?;
                Some(
                    self.repo()
                        .find_tree(o)
                        .map_err(|e| GitError::Internal(e.to_string()))?,
                )
            }
            None => None,
        };

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

        // git2 requires at least one tree to be Some
        let diff = match (old_tree.as_ref(), new_tree.as_ref()) {
            (Some(old), Some(new)) => {
                self.repo()
                    .diff_tree_to_tree(Some(old), Some(new), Some(&mut git_opts))
            }
            (Some(old), None) => {
                self.repo()
                    .diff_tree_to_tree(Some(old), None, Some(&mut git_opts))
            }
            (None, Some(new)) => {
                self.repo()
                    .diff_tree_to_tree(None, Some(new), Some(&mut git_opts))
            }
            (None, None) => {
                return Err(GitError::Internal(
                    "Both old_tree and new_tree are None".into(),
                ));
            }
        }
        .map_err(|e| GitError::Internal(e.to_string()))?;

        build_diff_result(&diff)
    }

    pub fn diff_commit_to_workdir(
        &self,
        commit: &CommitOid,
        opts: Option<DiffOptions>,
    ) -> GitResult<DiffResult> {
        let oid = commit
            .to_oid()
            .map_err(|_| GitError::InvalidOid(commit.to_string()))?;

        let commit = self
            .repo()
            .find_commit(oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let tree = self
            .repo()
            .find_tree(commit.tree_id())
            .map_err(|e| GitError::Internal(e.to_string()))?;

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

        let diff = self
            .repo()
            .diff_tree_to_workdir(Some(&tree), Some(&mut git_opts))
            .map_err(|e| GitError::Internal(e.to_string()))?;

        build_diff_result(&diff)
    }

    pub fn diff_commit_to_index(
        &self,
        commit: &CommitOid,
        opts: Option<DiffOptions>,
    ) -> GitResult<DiffResult> {
        let oid = commit
            .to_oid()
            .map_err(|_| GitError::InvalidOid(commit.to_string()))?;

        let commit = self
            .repo()
            .find_commit(oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let tree = self
            .repo()
            .find_tree(commit.tree_id())
            .map_err(|e| GitError::Internal(e.to_string()))?;

        // Get the index as a tree for comparison.
        let mut index = self
            .repo()
            .index()
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let index_tree_oid = index
            .write_tree()
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let index_tree = self
            .repo()
            .find_tree(index_tree_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;

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

        let diff = self
            .repo()
            .diff_tree_to_tree(Some(&tree), Some(&index_tree), Some(&mut git_opts))
            .map_err(|e| GitError::Internal(e.to_string()))?;

        build_diff_result(&diff)
    }

    pub fn diff_workdir_to_index(&self, opts: Option<DiffOptions>) -> GitResult<DiffResult> {
        let mut git_opts = opts
            .map(|o| o.to_git2())
            .unwrap_or_else(git2::DiffOptions::new);

        let diff = self
            .repo()
            .diff_tree_to_workdir(None, Some(&mut git_opts))
            .map_err(|e| GitError::Internal(e.to_string()))?;

        build_diff_result(&diff)
    }

    pub fn diff_index_to_tree(
        &self,
        tree: &CommitOid,
        opts: Option<DiffOptions>,
    ) -> GitResult<DiffResult> {
        let oid = tree
            .to_oid()
            .map_err(|_| GitError::InvalidOid(tree.to_string()))?;

        let tree = self
            .repo()
            .find_tree(oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;

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

        let diff = self
            .repo()
            .diff_tree_to_tree(Some(&tree), None, Some(&mut git_opts))
            .map_err(|e| GitError::Internal(e.to_string()))?;

        build_diff_result(&diff)
    }

    pub fn diff_stats(&self, old_tree: &CommitOid, new_tree: &CommitOid) -> GitResult<DiffStats> {
        let old_oid = old_tree
            .to_oid()
            .map_err(|_| GitError::InvalidOid(old_tree.to_string()))?;
        let new_oid = new_tree
            .to_oid()
            .map_err(|_| GitError::InvalidOid(new_tree.to_string()))?;

        let old_tree = self
            .repo()
            .find_tree(old_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let new_tree = self
            .repo()
            .find_tree(new_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let diff = self
            .repo()
            .diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let stats = diff
            .stats()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(DiffStats::from_git2(&stats))
    }

    pub fn diff_patch_id(&self, old_tree: &CommitOid, new_tree: &CommitOid) -> GitResult<String> {
        let old_oid = old_tree
            .to_oid()
            .map_err(|_| GitError::InvalidOid(old_tree.to_string()))?;
        let new_oid = new_tree
            .to_oid()
            .map_err(|_| GitError::InvalidOid(new_tree.to_string()))?;

        let old_tree = self
            .repo()
            .find_tree(old_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let new_tree = self
            .repo()
            .find_tree(new_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let diff = self
            .repo()
            .diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let patch_id = diff
            .patchid(None)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(patch_id.to_string())
    }
}

fn build_diff_result(diff: &git2::Diff<'_>) -> GitResult<DiffResult> {
    let stats = diff
        .stats()
        .map_err(|e| GitError::Internal(e.to_string()))?;

    let delta_count = diff.deltas().len();
    let deltas: RefCell<Vec<DiffDelta>> = RefCell::new(Vec::with_capacity(delta_count));
    let delta_hunks: RefCell<Vec<DiffHunk>> = RefCell::new(Vec::new());
    let delta_lines: RefCell<Vec<DiffLine>> = RefCell::new(Vec::new());
    let counter: RefCell<usize> = RefCell::new(0);

    let mut file_cb = |_delta: git2::DiffDelta<'_>, _progress: f32| -> bool {
        let count = *counter.borrow();
        if count > 0 {
            let prev_idx = count - 1;
            let hunks = delta_hunks.take();
            let lines = delta_lines.take();
            if let Some(prev_delta) = diff.get_delta(prev_idx) {
                deltas
                    .borrow_mut()
                    .push(DiffDelta::from_git2(&prev_delta, hunks, lines));
            }
        }
        *counter.borrow_mut() = count + 1;
        true
    };

    let mut hunk_cb = |_delta: git2::DiffDelta<'_>, hunk: git2::DiffHunk<'_>| -> bool {
        delta_hunks.borrow_mut().push(DiffHunk::from_git2(&hunk));
        true
    };

    let mut line_cb = |_delta: git2::DiffDelta<'_>,
                       _hunk: Option<git2::DiffHunk<'_>>,
                       line: git2::DiffLine<'_>|
     -> bool {
        delta_lines.borrow_mut().push(DiffLine::from_git2(&line));
        true
    };

    diff.foreach(&mut file_cb, None, Some(&mut hunk_cb), Some(&mut line_cb))
        .map_err(|e| GitError::Internal(e.to_string()))?;
    {
        let count = *counter.borrow();
        if count > 0 {
            let last_idx = count - 1;
            let hunks = delta_hunks.take();
            let lines = delta_lines.take();
            if let Some(last_delta) = diff.get_delta(last_idx) {
                deltas
                    .borrow_mut()
                    .push(DiffDelta::from_git2(&last_delta, hunks, lines));
            }
        }
    }

    Ok(DiffResult {
        stats: DiffStats::from_git2(&stats),
        deltas: deltas.into_inner(),
    })
}

/// The algorithm walks the unified diff line-by-line and produces rows where:
/// - **Added** lines appear only on the right side.
/// - **Deleted** lines appear only on the left side.
/// - When a deletion is immediately followed by an addition they are rendered as a
///   **Modified** pair (two separate rows).
/// - **Context** lines appear on both sides with the same content.
/// - **Empty** filler rows are inserted so that left and right line numbers stay aligned.
pub fn diff_to_side_by_side(diff: &DiffResult) -> SideBySideDiffResult {
    let mut files: Vec<SideBySideFile> = Vec::with_capacity(diff.deltas.len());
    let mut total_additions = 0usize;
    let mut total_deletions = 0usize;

    for delta in &diff.deltas {
        let (path, is_binary, is_rename) =
            if delta.status == crate::diff::types::DiffDeltaStatus::Renamed {
                (
                    delta
                        .new_file
                        .path
                        .clone()
                        .or_else(|| delta.old_file.path.clone())
                        .unwrap_or_default(),
                    delta.new_file.is_binary || delta.old_file.is_binary,
                    true,
                )
            } else {
                (
                    delta
                        .new_file
                        .path
                        .clone()
                        .or_else(|| delta.old_file.path.clone())
                        .unwrap_or_default(),
                    delta.new_file.is_binary,
                    false,
                )
            };

        if is_binary {
            files.push(SideBySideFile {
                path,
                additions: 0,
                deletions: 0,
                is_binary: true,
                is_rename,
                lines: vec![],
            });
            continue;
        }

        let lines = build_side_by_side_lines(&delta.lines);
        let additions = lines
            .iter()
            .filter(|l| matches!(l.change_type, SideBySideChangeType::Added))
            .count();
        let deletions = lines
            .iter()
            .filter(|l| matches!(l.change_type, SideBySideChangeType::Removed))
            .count();

        total_additions += additions;
        total_deletions += deletions;

        files.push(SideBySideFile {
            path,
            additions,
            deletions,
            is_binary: false,
            is_rename,
            lines,
        });
    }

    SideBySideDiffResult {
        files,
        total_additions,
        total_deletions,
    }
}

fn build_side_by_side_lines(unified: &[DiffLine]) -> Vec<SideBySideLine> {
    let mut result: Vec<SideBySideLine> = Vec::with_capacity(unified.len() * 2);
    let mut i = 0;

    while i < unified.len() {
        let line = &unified[i];

        match line.origin {
            '+' => {
                // Collect a run of consecutive additions.
                let mut added_lines: Vec<&DiffLine> = vec![line];
                while i + 1 < unified.len() && unified[i + 1].origin == '+' {
                    i += 1;
                    added_lines.push(&unified[i]);
                }

                // Peek at the previous deletion run to pair with additions.
                let mut deleted_lines: Vec<&DiffLine> = vec![];
                // Backtrack to find deletions right before this run.
                let Some(j_base) = i.checked_sub(added_lines.len()) else {
                    // Additions at start of diff — no preceding deletions to pair with.
                    // Emit all as unpaired Added below.
                    for k in 0..added_lines.len() {
                        let add = added_lines[k];
                        result.push(SideBySideLine {
                            left_line_no: None,
                            right_line_no: add.new_lineno,
                            left_content: String::new(),
                            right_content: add.content.clone(),
                            change_type: SideBySideChangeType::Added,
                        });
                    }
                    i += 1;
                    continue;
                };
                let mut j = j_base;
                while j > 0 && unified[j].origin == '-' {
                    j -= 1;
                }
                // Collect deletions from j+1 up to (but not including) the first addition.
                let del_start = if j > 0 { j + 1 } else { 0 };
                for k in del_start..i {
                    if unified[k].origin == '-' {
                        deleted_lines.push(&unified[k]);
                    }
                }

                // If we have paired deletions, emit them as Modified pairs.
                let pairs = deleted_lines.len().min(added_lines.len());
                for k in 0..pairs {
                    let del = deleted_lines[k];
                    let add = added_lines[k];
                    result.push(SideBySideLine {
                        left_line_no: del.old_lineno,
                        right_line_no: add.new_lineno,
                        left_content: del.content.clone(),
                        right_content: add.content.clone(),
                        change_type: SideBySideChangeType::Modified,
                    });
                }

                // Remaining unpaired additions.
                for k in pairs..added_lines.len() {
                    let add = added_lines[k];
                    result.push(SideBySideLine {
                        left_line_no: None,
                        right_line_no: add.new_lineno,
                        left_content: String::new(),
                        right_content: add.content.clone(),
                        change_type: SideBySideChangeType::Added,
                    });
                }

                // Remaining unpaired deletions (only possible if deletions > additions).
                for k in pairs..deleted_lines.len() {
                    let del = deleted_lines[k];
                    result.push(SideBySideLine {
                        left_line_no: del.old_lineno,
                        right_line_no: None,
                        left_content: del.content.clone(),
                        right_content: String::new(),
                        change_type: SideBySideChangeType::Removed,
                    });
                }
            }
            '-' => {
                // Collect a run of consecutive deletions.
                let mut deleted_lines: Vec<&DiffLine> = vec![line];
                while i + 1 < unified.len() && unified[i + 1].origin == '-' {
                    i += 1;
                    deleted_lines.push(&unified[i]);
                }

                // Emit each deletion (unless already paired above).
                // We defer pairing to the addition-handling block above,
                // so here we just emit unpaired deletions.
                for del in &deleted_lines {
                    result.push(SideBySideLine {
                        left_line_no: del.old_lineno,
                        right_line_no: None,
                        left_content: del.content.clone(),
                        right_content: String::new(),
                        change_type: SideBySideChangeType::Removed,
                    });
                }
            }
            _ => {
                // Context line — appears on both sides.
                result.push(SideBySideLine {
                    left_line_no: line.old_lineno,
                    right_line_no: line.new_lineno,
                    left_content: line.content.clone(),
                    right_content: line.content.clone(),
                    change_type: SideBySideChangeType::Unchanged,
                });
            }
        }

        i += 1;
    }

    result
}
