use serde::{Deserialize, Serialize};
use similar::{ChangeTag, TextDiff};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiffAlgorithm {
    Myers,
    Patience,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiffLineType {
    Context,
    Addition,
    Deletion,
    HunkHeader,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffLine {
    pub line_type: DiffLineType,
    pub old_lineno: Option<usize>,
    pub new_lineno: Option<usize>,
    pub content: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffHunk {
    pub header: String,
    pub old_start: usize,
    pub old_lines: usize,
    pub new_start: usize,
    pub new_lines: usize,
    pub lines: Vec<DiffLine>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileDiff {
    pub old_path: Option<String>,
    pub new_path: Option<String>,
    pub is_binary: bool,
    pub hunks: Vec<DiffHunk>,
    pub additions: usize,
    pub deletions: usize,
}

pub struct DiffEngine;

impl DiffEngine {
    pub fn diff_strings(
        old_text: &str,
        new_text: &str,
        old_path: Option<&str>,
        new_path: Option<&str>,
    ) -> FileDiff {
        let diff = TextDiff::from_lines(old_text, new_text);
        let mut hunks = Vec::new();
        let mut total_additions = 0;
        let mut total_deletions = 0;

        for group in diff.grouped_ops(3) {
            let mut lines = Vec::new();
            let mut old_start = 0;
            let mut old_count = 0;
            let mut new_start = 0;
            let mut new_count = 0;

            for (idx, op) in group.iter().enumerate() {
                if idx == 0 {
                    old_start = op.old_range().start + 1;
                    new_start = op.new_range().start + 1;
                }
                old_count += op.old_range().len();
                new_count += op.new_range().len();

                for change in diff.iter_changes(op) {
                    let (line_type, old_no, new_no) = match change.tag() {
                        ChangeTag::Equal => (
                            DiffLineType::Context,
                            change.old_index().map(|i| i + 1),
                            change.new_index().map(|i| i + 1),
                        ),
                        ChangeTag::Delete => {
                            total_deletions += 1;
                            (
                                DiffLineType::Deletion,
                                change.old_index().map(|i| i + 1),
                                None,
                            )
                        }
                        ChangeTag::Insert => {
                            total_additions += 1;
                            (
                                DiffLineType::Addition,
                                None,
                                change.new_index().map(|i| i + 1),
                            )
                        }
                    };

                    lines.push(DiffLine {
                        line_type,
                        old_lineno: old_no,
                        new_lineno: new_no,
                        content: change
                            .value()
                            .trim_end_matches(&['\r', '\n'][..])
                            .to_string(),
                    });
                }
            }

            let header = format!(
                "@@ -{},{} +{},{} @@",
                old_start, old_count, new_start, new_count
            );
            hunks.push(DiffHunk {
                header,
                old_start,
                old_lines: old_count,
                new_start,
                new_lines: new_count,
                lines,
            });
        }

        FileDiff {
            old_path: old_path.map(|s| s.to_string()),
            new_path: new_path.map(|s| s.to_string()),
            is_binary: false,
            hunks,
            additions: total_additions,
            deletions: total_deletions,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{DiffEngine, DiffLineType};

    #[test]
    fn reports_line_changes_and_hunks() {
        let diff =
            DiffEngine::diff_strings("one\ntwo\n", "one\nthree\n", Some("a.txt"), Some("a.txt"));
        assert_eq!(diff.additions, 1);
        assert_eq!(diff.deletions, 1);
        assert!(!diff.hunks.is_empty());
        assert!(diff.hunks[0]
            .lines
            .iter()
            .any(|line| line.line_type == DiffLineType::Addition));
    }
}