use std::path::Path;

use git2::{Delta, DiffOptions, Repository, Sort};

use astmap_core::model::FileChangeStatus;
use astmap_core::{DiffError, VcsFileChange, VcsProvider};

pub struct Git2VcsProvider {
    repo: Repository,
}

impl Git2VcsProvider {
    pub fn open(project_dir: &Path) -> Result<Self, DiffError> {
        let repo = Repository::open(project_dir).map_err(|_| DiffError::NotGitRepo)?;
        Ok(Self { repo })
    }
}

/// Convert a git2 error to a DiffError::Vcs.
fn vcs_err(e: git2::Error) -> DiffError {
    DiffError::Vcs(e.message().to_string())
}

impl VcsProvider for Git2VcsProvider {
    fn working_tree_changes(
        &self,
        base_ref: Option<&str>,
    ) -> Result<Vec<VcsFileChange>, DiffError> {
        let tree = if let Some(refspec) = base_ref {
            let obj = self.repo.revparse_single(refspec).map_err(vcs_err)?;
            obj.peel_to_tree().map_err(vcs_err)?
        } else {
            let head = self.repo.head().map_err(vcs_err)?;
            head.peel_to_tree().map_err(vcs_err)?
        };

        let mut opts = DiffOptions::new();
        opts.include_untracked(true);

        let diff = self
            .repo
            .diff_tree_to_workdir_with_index(Some(&tree), Some(&mut opts))
            .map_err(vcs_err)?;

        collect_diff_changes(&diff)
    }

    fn changes_between_refs(
        &self,
        from_ref: &str,
        to_ref: &str,
    ) -> Result<Vec<VcsFileChange>, DiffError> {
        let from_obj = self.repo.revparse_single(from_ref).map_err(vcs_err)?;
        let from_tree = from_obj.peel_to_tree().map_err(vcs_err)?;

        let to_obj = self.repo.revparse_single(to_ref).map_err(vcs_err)?;
        let to_tree = to_obj.peel_to_tree().map_err(vcs_err)?;

        let diff = self
            .repo
            .diff_tree_to_tree(Some(&from_tree), Some(&to_tree), None)
            .map_err(vcs_err)?;

        collect_diff_changes(&diff)
    }

    fn read_file_at_ref(&self, path: &str, git_ref: &str) -> Result<Option<String>, DiffError> {
        let obj = self.repo.revparse_single(git_ref).map_err(vcs_err)?;
        let tree = obj.peel_to_tree().map_err(vcs_err)?;

        let entry = match tree.get_path(Path::new(path)) {
            Ok(e) => e,
            Err(_) => return Ok(None),
        };

        let blob = match self.repo.find_blob(entry.id()) {
            Ok(b) => b,
            Err(_) => return Ok(None),
        };

        match std::str::from_utf8(blob.content()) {
            Ok(s) => Ok(Some(s.to_string())),
            Err(_) => Ok(None), // binary file
        }
    }

    fn commit_count(&self, from_ref: &str, to_ref: &str) -> Result<usize, DiffError> {
        let from_obj = self.repo.revparse_single(from_ref).map_err(vcs_err)?;
        let to_obj = self.repo.revparse_single(to_ref).map_err(vcs_err)?;

        let from_oid = from_obj.peel_to_commit().map(|c| c.id()).map_err(vcs_err)?;
        let to_oid = to_obj.peel_to_commit().map(|c| c.id()).map_err(vcs_err)?;

        let mut revwalk = self.repo.revwalk().map_err(vcs_err)?;
        revwalk.push(to_oid).map_err(vcs_err)?;
        revwalk.hide(from_oid).map_err(vcs_err)?;

        Ok(revwalk.count())
    }

    fn resolve_ref(&self, refspec: &str) -> Result<bool, DiffError> {
        Ok(self.repo.revparse_single(refspec).is_ok())
    }

    fn head_sha(&self) -> Result<String, DiffError> {
        let head = self.repo.head().map_err(vcs_err)?;
        let commit = head.peel_to_commit().map_err(vcs_err)?;
        Ok(commit.id().to_string())
    }

    fn root_commit_sha(&self) -> Result<Option<String>, DiffError> {
        let head = self.repo.head().map_err(vcs_err)?;
        let commit = head.peel_to_commit().map_err(vcs_err)?;

        let mut revwalk = self.repo.revwalk().map_err(vcs_err)?;
        revwalk.push(commit.id()).map_err(vcs_err)?;
        revwalk.set_sorting(Sort::REVERSE).map_err(vcs_err)?;

        match revwalk.next() {
            Some(Ok(root_oid)) => Ok(Some(root_oid.to_string())),
            _ => Ok(None),
        }
    }
}

/// Collect VcsFileChange entries from a git2 Diff, including hunk line ranges.
fn collect_diff_changes(diff: &git2::Diff) -> Result<Vec<VcsFileChange>, DiffError> {
    // First pass: collect delta metadata
    let mut changes: Vec<VcsFileChange> = Vec::new();

    for delta_idx in 0..diff.deltas().len() {
        let Some(delta) = diff.get_delta(delta_idx) else {
            continue;
        };

        let status = match delta.status() {
            Delta::Added | Delta::Untracked => FileChangeStatus::Added,
            Delta::Deleted => FileChangeStatus::Deleted,
            Delta::Modified => FileChangeStatus::Modified,
            Delta::Renamed => FileChangeStatus::Renamed,
            Delta::Copied => FileChangeStatus::Modified,
            _ => continue,
        };

        let new_path = delta
            .new_file()
            .path()
            .map(|p| p.to_string_lossy().to_string());
        let old_path = delta
            .old_file()
            .path()
            .map(|p| p.to_string_lossy().to_string());

        let path = if status == FileChangeStatus::Deleted {
            match old_path.clone() {
                Some(p) => p,
                None => continue,
            }
        } else {
            match new_path {
                Some(p) => p,
                None => continue,
            }
        };

        changes.push(VcsFileChange {
            path,
            old_path,
            status,
            changed_lines: Vec::new(), // filled in second pass
        });
    }

    // Second pass: collect hunk line ranges for modified/renamed files.
    // Build a path->index map so we can assign hunk ranges to the right change.
    let mut path_to_idx: std::collections::HashMap<String, usize> =
        std::collections::HashMap::new();
    for (idx, change) in changes.iter().enumerate() {
        match change.status {
            FileChangeStatus::Modified | FileChangeStatus::Renamed => {
                path_to_idx.insert(change.path.clone(), idx);
            }
            _ => {}
        }
    }

    if !path_to_idx.is_empty() {
        let _ = diff.foreach(
            &mut |_, _| true,
            None,
            Some(&mut |d, hunk| {
                let hunk_path = d.new_file().path().map(|p| p.to_string_lossy().to_string());
                if let Some(ref hp) = hunk_path {
                    if let Some(&idx) = path_to_idx.get(hp.as_str()) {
                        let start = hunk.new_start() as i64;
                        let end = start + hunk.new_lines() as i64 - 1;
                        changes[idx].changed_lines.push((start, end.max(start)));
                    }
                }
                true
            }),
            None,
        );
    }

    Ok(changes)
}
