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

use crate::{
    bare::GitBare,
    cmd::{
        diff::{
            DiffDelta, DiffDeltaStatus, DiffFile, DiffHunk, DiffLine,
            DiffOptions, DiffResult, DiffStats,
        },
        oid::ObjectId,
    },
    errors::{GitError, GitResult},
};
pub fn peel_to_tree(
    repo: &gix::Repository,
    oid: ObjectId,
) -> GitResult<gix::Tree<'_>> {
    let gix_id: gix::hash::ObjectId = (&oid).try_into()?;
    if let Ok(tree) = repo.find_tree(gix_id) {
        Ok(tree)
    } else {
        let commit = repo
            .find_commit(gix_id)
            .map_err(|e| GitError::Gix(e.to_string()))?;
        let tree_id = commit
            .tree_id()
            .map_err(|e| GitError::Gix(e.to_string()))?
            .detach();
        repo.find_tree(tree_id)
            .map_err(|e| GitError::Gix(e.to_string()))
    }
}
pub fn matches_pathspec(pathspec: &[String], path: &str) -> bool {
    if pathspec.is_empty() {
        return true;
    }
    pathspec
        .iter()
        .any(|spec| path == spec || path.starts_with(spec))
}
pub fn change_to_delta(
    change: &gix::diff::tree_with_rewrites::Change,
) -> DiffDelta {
    use gix::diff::tree_with_rewrites::Change;

    match change {
        Change::Addition { location, id, .. } => {
            let path = location.to_str().unwrap_or("").to_string();
            DiffDelta {
                status: DiffDeltaStatus::Added,
                old_file: DiffFile {
                    oid: None,
                    path: Some(path.clone()),
                    size: 0,
                    is_binary: false,
                },
                new_file: DiffFile {
                    oid: oid_to_option(id),
                    path: Some(path),
                    size: 0,
                    is_binary: false,
                },
                nfiles: 1,
                hunks: Vec::new(),
                lines: Vec::new(),
            }
        }
        Change::Deletion { location, id, .. } => {
            let path = location.to_str().unwrap_or("").to_string();
            DiffDelta {
                status: DiffDeltaStatus::Deleted,
                old_file: DiffFile {
                    oid: oid_to_option(id),
                    path: Some(path.clone()),
                    size: 0,
                    is_binary: false,
                },
                new_file: DiffFile {
                    oid: None,
                    path: Some(path),
                    size: 0,
                    is_binary: false,
                },
                nfiles: 1,
                hunks: Vec::new(),
                lines: Vec::new(),
            }
        }
        Change::Modification {
            location,
            previous_entry_mode,
            previous_id,
            entry_mode,
            id,
        } => {
            let path = location.to_str().unwrap_or("").to_string();
            let status = if previous_entry_mode.kind() != entry_mode.kind() {
                DiffDeltaStatus::Typechange
            } else {
                DiffDeltaStatus::Modified
            };
            DiffDelta {
                status,
                old_file: DiffFile {
                    oid: oid_to_option(previous_id),
                    path: Some(path.clone()),
                    size: 0,
                    is_binary: false,
                },
                new_file: DiffFile {
                    oid: oid_to_option(id),
                    path: Some(path),
                    size: 0,
                    is_binary: false,
                },
                nfiles: 1,
                hunks: Vec::new(),
                lines: Vec::new(),
            }
        }
        Change::Rewrite {
            source_location,
            source_id,
            location,
            id,
            copy,
            ..
        } => {
            let old_path = source_location.to_str().unwrap_or("").to_string();
            let new_path = location.to_str().unwrap_or("").to_string();
            DiffDelta {
                status: if *copy {
                    DiffDeltaStatus::Copied
                } else {
                    DiffDeltaStatus::Renamed
                },
                old_file: DiffFile {
                    oid: oid_to_option(source_id),
                    path: Some(old_path),
                    size: 0,
                    is_binary: false,
                },
                new_file: DiffFile {
                    oid: oid_to_option(id),
                    path: Some(new_path),
                    size: 0,
                    is_binary: false,
                },
                nfiles: 2,
                hunks: Vec::new(),
                lines: Vec::new(),
            }
        }
    }
}

pub fn oid_to_option(id: &gix::hash::oid) -> Option<ObjectId> {
    if id.is_null() {
        None
    } else {
        Some(ObjectId::new(id.to_hex().to_string()))
    }
}
pub struct HunkCollector {
    hunks: Vec<DiffHunk>,
    lines: Vec<DiffLine>,
    current_old_lineno: u32,
    current_new_lineno: u32,
}

impl HunkCollector {
    pub fn new() -> Self {
        HunkCollector {
            hunks: Vec::new(),
            lines: Vec::new(),
            current_old_lineno: 0,
            current_new_lineno: 0,
        }
    }
}

impl ConsumeHunk for HunkCollector {
    type Out = (Vec<DiffHunk>, Vec<DiffLine>);

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

        self.hunks.push(DiffHunk {
            old_start: header.before_hunk_start,
            old_lines: header.before_hunk_len,
            new_start: header.after_hunk_start,
            new_lines: header.after_hunk_len,
            header: format!(
                "@@ -{},{} +{},{} @@",
                header.before_hunk_start,
                header.before_hunk_len,
                header.after_hunk_start,
                header.after_hunk_len
            ),
        });

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

            let (old_lineno, new_lineno) = match kind {
                DiffLineKind::Context => {
                    let old = Some(self.current_old_lineno);
                    let new = Some(self.current_new_lineno);
                    self.current_old_lineno += 1;
                    self.current_new_lineno += 1;
                    (old, new)
                }
                DiffLineKind::Add => {
                    let new = Some(self.current_new_lineno);
                    self.current_new_lineno += 1;
                    (None, new)
                }
                DiffLineKind::Remove => {
                    let old = self.current_old_lineno;
                    self.current_old_lineno += 1;
                    (Some(old), None)
                }
            };

            self.lines.push(DiffLine {
                content: content_str,
                origin,
                old_lineno,
                new_lineno,
                num_lines: 1,
                content_offset: -1,
            });
        }

        Ok(())
    }

    fn finish(self) -> (Vec<DiffHunk>, Vec<DiffLine>) {
        (self.hunks, self.lines)
    }
}

impl GitBare {
    pub fn diff_tree_to_tree(
        &self,
        old_tree: ObjectId,
        new_tree: ObjectId,
        opts: Option<DiffOptions>,
    ) -> GitResult<DiffResult> {
        let repo = self.gix_repo()?;
        let options = opts.unwrap_or_default();

        let old_tree_obj = peel_to_tree(&repo, old_tree)?;
        let new_tree_obj = peel_to_tree(&repo, new_tree)?;

        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);

            // Skip directories — only diff blobs
            if change.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;

                        if options.context_lines > 0 {
                            let ctx = ContextSize::symmetrical(
                                options.context_lines.max(3),
                            );
                            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 })
    }
}
