use gix::{
    bstr::ByteSlice,
    diff::blob::unified_diff::{
        ConsumeHunk, ContextSize, DiffLineKind, HunkHeader,
    },
};

use crate::{
    bare::GitBare,
    cmd::{
        diff::{
            DiffDeltaStatus, DiffOptions, DiffResult, DiffStats,
            SideBySideChangeType, SideBySideDiffResult, SideBySideFile,
            SideBySideLine,
            diff_tree_to_tree::{
                HunkCollector, change_to_delta, matches_pathspec, peel_to_tree,
            },
        },
        oid::ObjectId,
    },
    errors::{GitError, GitResult},
};

impl GitBare {
    pub fn diff_patch(
        &self,
        old_commit: ObjectId,
        new_commit: ObjectId,
        opts: Option<DiffOptions>,
    ) -> GitResult<DiffResult> {
        let repo = self.gix_repo()?;
        let options = opts.unwrap_or_default();
        let context_lines = options.context_lines.max(3);

        let old_tree_obj = peel_to_tree(&repo, old_commit)?;
        let new_tree_obj = peel_to_tree(&repo, new_commit)?;

        let mut diff_opts = gix::diff::Options::default();
        diff_opts.track_path();

        let changes = repo
            .diff_tree_to_tree(&old_tree_obj, &new_tree_obj, Some(diff_opts))
            .map_err(|e| GitError::Gix(e.to_string()))?;

        let mut resource_cache = repo
            .diff_resource_cache_for_tree_diff()
            .map_err(|e| GitError::Gix(e.to_string()))?;

        let mut deltas = Vec::new();
        let mut stats = DiffStats {
            files_changed: 0,
            insertions: 0,
            deletions: 0,
        };

        for change in &changes {
            let location = change.location().to_str().unwrap_or("");
            if !matches_pathspec(&options.pathspec, location) {
                continue;
            }

            let mut delta = change_to_delta(change);

            // Only diff blobs — skip trees (directories)
            let entry_mode = change.entry_mode();
            if entry_mode.is_tree() {
                resource_cache.clear_resource_cache_keep_allocation();
                continue;
            }

            resource_cache
                .set_resource_by_change(change.to_ref(), &repo.objects)
                .map_err(|e| GitError::Gix(e.to_string()))?;

            let is_binary = {
                use gix::diff::blob::platform::prepare_diff::Operation;

                let prep = resource_cache
                    .prepare_diff()
                    .map_err(|e| GitError::Gix(e.to_string()))?;

                match prep.operation {
                    Operation::InternalDiff { algorithm } => {
                        let input = prep.interned_input();
                        let diff = gix::diff::blob::diff_with_slider_heuristics(
                            algorithm, &input,
                        );

                        stats.files_changed += 1;
                        stats.insertions += diff.count_additions() as usize;
                        stats.deletions += diff.count_removals() as usize;

                        let ctx = ContextSize::symmetrical(context_lines);
                        let collector = HunkCollector::new();
                        let unified = gix::diff::blob::UnifiedDiff::new(
                            &diff, &input, collector, ctx,
                        );
                        let (hunks, lines) = unified
                            .consume()
                            .map_err(|e| GitError::Gix(e.to_string()))?;
                        delta.hunks = hunks;
                        delta.lines = lines;

                        false
                    }
                    Operation::SourceOrDestinationIsBinary => {
                        stats.files_changed += 1;
                        true
                    }
                    Operation::ExternalCommand { .. } => {
                        stats.files_changed += 1;
                        false
                    }
                }
            };

            if is_binary {
                delta.old_file.is_binary = true;
                delta.new_file.is_binary = true;
            }

            resource_cache.clear_resource_cache_keep_allocation();
            deltas.push(delta);
        }

        Ok(DiffResult { stats, deltas })
    }

    pub fn diff_patch_side_by_side(
        &self,
        old_commit: ObjectId,
        new_commit: ObjectId,
        opts: Option<DiffOptions>,
    ) -> GitResult<SideBySideDiffResult> {
        let repo = self.gix_repo()?;
        let options = opts.unwrap_or_default();
        let context_lines = options.context_lines.max(3);

        let old_tree_obj = peel_to_tree(&repo, old_commit)?;
        let new_tree_obj = peel_to_tree(&repo, new_commit)?;

        let mut diff_opts = gix::diff::Options::default();
        diff_opts.track_path();

        let changes = repo
            .diff_tree_to_tree(&old_tree_obj, &new_tree_obj, Some(diff_opts))
            .map_err(|e| GitError::Gix(e.to_string()))?;

        let mut resource_cache = repo
            .diff_resource_cache_for_tree_diff()
            .map_err(|e| GitError::Gix(e.to_string()))?;

        let mut files: Vec<SideBySideFile> = Vec::new();
        let mut total_additions = 0;
        let mut total_deletions = 0;

        for change in &changes {
            let location = change.location().to_str().unwrap_or("");
            if !matches_pathspec(&options.pathspec, location) {
                continue;
            }

            let delta = change_to_delta(change);
            let is_rename = delta.status == DiffDeltaStatus::Renamed;
            let path = delta.new_file.path.clone().unwrap_or_default();

            // Skip directories — only diff blobs
            if change.entry_mode().is_tree() {
                total_additions += 0;
                total_deletions += 0;
                files.push(SideBySideFile {
                    path,
                    additions: 0,
                    deletions: 0,
                    is_binary: false,
                    is_rename,
                    lines: Vec::new(),
                });
                resource_cache.clear_resource_cache_keep_allocation();
                continue;
            }

            resource_cache
                .set_resource_by_change(change.to_ref(), &repo.objects)
                .map_err(|e| GitError::Gix(e.to_string()))?;

            let (file_additions, file_deletions, is_binary, sbs_lines) = {
                use gix::diff::blob::platform::prepare_diff::Operation;

                let prep = resource_cache
                    .prepare_diff()
                    .map_err(|e| GitError::Gix(e.to_string()))?;

                match prep.operation {
                    Operation::InternalDiff { algorithm } => {
                        let input = prep.interned_input();
                        let diff = gix::diff::blob::diff_with_slider_heuristics(
                            algorithm, &input,
                        );

                        let adds = diff.count_additions() as usize;
                        let dels = diff.count_removals() as usize;

                        let ctx = ContextSize::symmetrical(context_lines);
                        let collector = SideBySideCollector::new();
                        let unified = gix::diff::blob::UnifiedDiff::new(
                            &diff, &input, collector, ctx,
                        );
                        let lines = unified
                            .consume()
                            .map_err(|e| GitError::Gix(e.to_string()))?;

                        (adds, dels, false, lines)
                    }
                    Operation::SourceOrDestinationIsBinary => {
                        (0, 0, true, Vec::new())
                    }
                    Operation::ExternalCommand { .. } => {
                        (0, 0, false, Vec::new())
                    }
                }
            };

            total_additions += file_additions;
            total_deletions += file_deletions;

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

            resource_cache.clear_resource_cache_keep_allocation();
        }

        Ok(SideBySideDiffResult {
            files,
            total_additions,
            total_deletions,
        })
    }
}
struct SideBySideCollector {
    lines: Vec<SideBySideLine>,
    pending_removed: Vec<(u32, String)>,
    pending_added: Vec<(u32, String)>,
    current_old_lineno: u32,
    current_new_lineno: u32,
}

impl SideBySideCollector {
    fn new() -> Self {
        SideBySideCollector {
            lines: Vec::new(),
            pending_removed: Vec::new(),
            pending_added: Vec::new(),
            current_old_lineno: 0,
            current_new_lineno: 0,
        }
    }

    fn flush_pending(&mut self) {
        let removed_count = self.pending_removed.len();
        let added_count = self.pending_added.len();
        let common = removed_count.min(added_count);

        for i in 0..common {
            let (left_no, old_content) = &self.pending_removed[i];
            let (right_no, new_content) = &self.pending_added[i];

            let change_type = if old_content == new_content {
                SideBySideChangeType::Unchanged
            } else {
                SideBySideChangeType::Modified
            };

            self.lines.push(SideBySideLine {
                left_line_no: Some(*left_no),
                right_line_no: Some(*right_no),
                left_content: old_content.clone(),
                right_content: new_content.clone(),
                change_type,
            });
        }

        for i in common..removed_count {
            let (left_no, old_content) = &self.pending_removed[i];
            self.lines.push(SideBySideLine {
                left_line_no: Some(*left_no),
                right_line_no: None,
                left_content: old_content.clone(),
                right_content: String::new(),
                change_type: SideBySideChangeType::Removed,
            });
        }

        for i in common..added_count {
            let (right_no, new_content) = &self.pending_added[i];
            self.lines.push(SideBySideLine {
                left_line_no: None,
                right_line_no: Some(*right_no),
                left_content: String::new(),
                right_content: new_content.clone(),
                change_type: SideBySideChangeType::Added,
            });
        }

        self.pending_removed.clear();
        self.pending_added.clear();
    }
}

impl ConsumeHunk for SideBySideCollector {
    type Out = Vec<SideBySideLine>;

    fn consume_hunk(
        &mut self,
        header: HunkHeader,
        entries: &[(DiffLineKind, &[u8])],
    ) -> std::io::Result<()> {
        self.flush_pending();
        self.current_old_lineno = header.before_hunk_start;
        self.current_new_lineno = header.after_hunk_start;

        for (kind, content) in entries {
            let content_str = String::from_utf8_lossy(content).to_string();

            match kind {
                DiffLineKind::Context => {
                    self.flush_pending();
                    self.lines.push(SideBySideLine {
                        left_line_no: Some(self.current_old_lineno),
                        right_line_no: Some(self.current_new_lineno),
                        left_content: content_str.clone(),
                        right_content: content_str,
                        change_type: SideBySideChangeType::Unchanged,
                    });
                    self.current_old_lineno += 1;
                    self.current_new_lineno += 1;
                }
                DiffLineKind::Add => {
                    self.pending_added
                        .push((self.current_new_lineno, content_str));
                    self.current_new_lineno += 1;
                }
                DiffLineKind::Remove => {
                    self.pending_removed
                        .push((self.current_old_lineno, content_str));
                    self.current_old_lineno += 1;
                }
            }
        }

        Ok(())
    }

    fn finish(mut self) -> Vec<SideBySideLine> {
        self.flush_pending();
        self.lines
    }
}
