use gix::bstr::ByteSlice;

use crate::{
    bare::GitBare,
    cmd::{
        diff::{
            DiffOptions, DiffStats,
            diff_tree_to_tree::{matches_pathspec, peel_to_tree},
        },
        oid::ObjectId,
    },
    errors::{GitError, GitResult},
};

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

        let old_tree_obj = peel_to_tree(&repo, old_commit)?;
        let new_tree_obj = peel_to_tree(&repo, new_commit)?;
        let mut platform = old_tree_obj
            .changes()
            .map_err(|e| GitError::Gix(e.to_string()))?;
        platform.options(|opts| {
            opts.track_rewrites(None);
        });

        let gix_stats = platform
            .stats(&new_tree_obj)
            .map_err(|e| GitError::Gix(e.to_string()))?;
        if options.pathspec.is_empty() {
            Ok(DiffStats {
                files_changed: gix_stats.files_changed as usize,
                insertions: gix_stats.lines_added as usize,
                deletions: gix_stats.lines_removed as usize,
            })
        } else {
            let changes = repo
                .diff_tree_to_tree(&old_tree_obj, &new_tree_obj, None)
                .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 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;
                }

                // Skip directories — only diff blobs
                if change.entry_mode().is_tree() {
                    continue;
                }

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

                {
                    use gix::diff::blob::platform::prepare_diff::Operation;

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

                    if let Operation::InternalDiff { algorithm } =
                        prep.operation
                    {
                        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;
                    } else if let Operation::SourceOrDestinationIsBinary =
                        prep.operation
                    {
                        stats.files_changed += 1;
                    }
                }

                resource_cache.clear_resource_cache_keep_allocation();
            }

            Ok(stats)
        }
    }
}
