//! Blame operations.

use crate::commit::types::{CommitBlameHunk, CommitBlameLine, CommitOid};
use crate::{GitDomain, GitError, GitResult};

/// Options for blame operations.
#[derive(Debug, Clone, Default)]
pub struct BlameOptions {
    pub min_line: Option<usize>,
    pub max_line: Option<usize>,
    pub track_copies_same_file: bool,
    pub track_copies_same_commit_moves: bool,
    pub ignore_whitespace: bool,
}

impl BlameOptions {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn min_line(mut self, line: usize) -> Self {
        self.min_line = Some(line);
        self
    }

    pub fn max_line(mut self, line: usize) -> Self {
        self.max_line = Some(line);
        self
    }

    pub fn track_copies_same_file(mut self) -> Self {
        self.track_copies_same_file = true;
        self
    }

    pub fn track_copies_same_commit_moves(mut self) -> Self {
        self.track_copies_same_commit_moves = true;
        self
    }

    pub fn ignore_whitespace(mut self) -> Self {
        self.ignore_whitespace = true;
        self
    }

    fn apply_to(&self, opts: &mut git2::BlameOptions) {
        if let Some(min) = self.min_line {
            opts.min_line(min);
        }
        if let Some(max) = self.max_line {
            opts.max_line(max);
        }
        if self.track_copies_same_file {
            opts.track_copies_same_file(true);
        }
        if self.track_copies_same_commit_moves {
            opts.track_copies_same_commit_moves(true);
        }
        if self.ignore_whitespace {
            opts.ignore_whitespace(true);
        }
    }
}

impl GitDomain {
    pub fn blame_file(
        &self,
        commit_oid: &CommitOid,
        path: &str,
        opts: Option<BlameOptions>,
    ) -> GitResult<Vec<CommitBlameHunk>> {
        let oid = commit_oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(commit_oid.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 blame_opts = git2::BlameOptions::new();
        if let Some(ref o) = opts {
            o.apply_to(&mut blame_opts);
        }

        let blame = self
            .repo()
            .blame_file(std::path::Path::new(path), Some(&mut blame_opts))
            .map_err(|e| GitError::Internal(e.to_string()))?;

        // Use get_index to iterate hunks
        let num_hunks = blame.len();
        let mut hunks: Vec<CommitBlameHunk> = Vec::with_capacity(num_hunks);

        for i in 0..num_hunks {
            if let Some(hunk) = blame.get_index(i) {
                hunks.push(CommitBlameHunk {
                    commit_oid: CommitOid::from_git2(hunk.orig_commit_id()),
                    final_start_line: hunk.final_start_line() as u32,
                    final_lines: hunk.lines_in_hunk() as u32,
                    orig_start_line: hunk.orig_start_line() as u32,
                    // NOTE: git2 0.20.4 BlameHunk does not expose the original hunk line count.
                    // orig_lines_in_hunk() is not available, so we reuse lines_in_hunk().
                    // The caller should prefer final_lines and orig_start_line for accuracy.
                    orig_lines: hunk.lines_in_hunk() as u32,
                    boundary: hunk.is_boundary(),
                    orig_path: hunk.path().map(|p| p.to_string_lossy().to_string()),
                });
            }
        }

        Ok(hunks)
    }

    /// Alias for blame_file.
    pub fn blame_path(
        &self,
        commit_oid: &CommitOid,
        path: &str,
        opts: Option<BlameOptions>,
    ) -> GitResult<Vec<CommitBlameHunk>> {
        self.blame_file(commit_oid, path, opts)
    }

    /// Reconstructs line attribution from hunk ranges.
    pub fn blame_lines(
        &self,
        commit_oid: &CommitOid,
        path: &str,
        opts: Option<BlameOptions>,
    ) -> GitResult<Vec<CommitBlameLine>> {
        let oid = commit_oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(commit_oid.to_string()))?;

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

        let mut blame_opts = git2::BlameOptions::new();
        if let Some(ref o) = opts {
            o.apply_to(&mut blame_opts);
        }

        let blame = self
            .repo()
            .blame_file(std::path::Path::new(path), Some(&mut blame_opts))
            .map_err(|e| GitError::Internal(e.to_string()))?;

        // Get file content for line text
        let tree = self
            .repo()
            .find_tree(commit.tree_id())
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let content_lines: Vec<String> = String::from_utf8_lossy(
            self.repo()
                .find_blob(
                    tree.get_path(std::path::Path::new(path))
                        .map_err(|_| {
                            GitError::ObjectNotFound(format!("file not found in commit: {}", path))
                        })?
                        .id(),
                )
                .map_err(|e| GitError::Internal(e.to_string()))?
                .content(),
        )
        .lines()
        .map(String::from)
        .collect();

        // Collect hunks
        let hunks: Vec<_> = (0..blame.len())
            .filter_map(|i| blame.get_index(i))
            .map(|h| {
                (
                    h.orig_commit_id(),
                    h.final_start_line(),
                    h.lines_in_hunk(),
                    h.path().map(|p| p.to_string_lossy().to_string()),
                )
            })
            .collect();

        let mut lines: Vec<CommitBlameLine> = Vec::with_capacity(content_lines.len());

        for (line_idx, content) in content_lines.iter().enumerate() {
            let line_no = line_idx as u32;
            let hunk = hunks.iter().find(|(_, start, count, _)| {
                let end = *start + *count;
                line_idx + 1 >= *start && line_idx + 1 < end
            });

            let (commit_oid, orig_path) = hunk
                .map(|(oid, _, _, path)| (CommitOid::from_git2(*oid), path.clone()))
                .unwrap_or_else(|| {
                    (
                        CommitOid::from_git2(git2::Oid::zero()),
                        Some(path.to_string()),
                    )
                });

            lines.push(CommitBlameLine {
                commit_oid,
                line_no,
                content: content.clone(),
                orig_path,
            });
        }

        Ok(lines)
    }

    pub fn blame_hunk_at(
        &self,
        commit_oid: &CommitOid,
        path: &str,
        line_no: usize,
    ) -> GitResult<CommitBlameHunk> {
        // Validate commit exists (git2 blame_file always operates on HEAD,
        // so we validate commit for early error rather than semantic correctness).
        let oid = commit_oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(commit_oid.to_string()))?;
        let _commit = self
            .repo()
            .find_commit(oid)
            .map_err(|e| GitError::ObjectNotFound(e.to_string()))?;
        let mut blame_opts = git2::BlameOptions::new();
        let blame = self
            .repo()
            .blame_file(std::path::Path::new(path), Some(&mut blame_opts))
            .map_err(|e| GitError::Internal(e.to_string()))?;

        // Use get_line to find the hunk at the given line
        let hunk_opt = blame.get_line(line_no);

        match hunk_opt {
            Some(hunk) => Ok(CommitBlameHunk {
                commit_oid: CommitOid::from_git2(hunk.orig_commit_id()),
                final_start_line: hunk.final_start_line() as u32,
                final_lines: hunk.lines_in_hunk() as u32,
                orig_start_line: hunk.orig_start_line() as u32,
                // NOTE: git2 0.20.4 BlameHunk does not expose the original hunk line count.
                // orig_lines_in_hunk() is not available, so we reuse lines_in_hunk().
                orig_lines: hunk.lines_in_hunk() as u32,
                boundary: hunk.is_boundary(),
                orig_path: hunk.path().map(|p| p.to_string_lossy().to_string()),
            }),
            None => Err(GitError::Internal(format!(
                "no blame hunk found for line {}",
                line_no
            ))),
        }
    }
}
