use std::fs;
use std::io;
use std::path::Path;

use imara_diff::{Algorithm, Diff, InternedInput};

use crate::types::{DiffChunk, FileDiff};

/// Strip \r so that CRLF and LF are treated identically.
fn normalize_line_endings(s: &str) -> String {
    if s.contains('\r') {
        s.replace('\r', "")
    } else {
        s.to_string()
    }
}

pub fn diff_files(left: &Path, right: &Path) -> io::Result<FileDiff> {
    let left_content = normalize_line_endings(&fs::read_to_string(left)?);
    let right_content = normalize_line_endings(&fs::read_to_string(right)?);

    let input = InternedInput::new(left_content.as_str(), right_content.as_str());
    let mut diff = Diff::compute(Algorithm::Histogram, &input);
    diff.postprocess_lines(&input);

    let mut chunks: Vec<DiffChunk> = Vec::new();
    let mut left_pos: u32 = 0;
    let mut right_pos: u32 = 0;

    for hunk in diff.hunks() {
        // Equal lines before this hunk
        if hunk.before.start > left_pos {
            let lines: Vec<String> = (left_pos..hunk.before.start)
                .map(|i| input.interner[input.before[i as usize]].to_string())
                .collect();
            chunks.push(DiffChunk::Equal {
                lines,
                left_start: left_pos as usize,
                right_start: right_pos as usize,
            });
        }

        let del_count = hunk.before.end - hunk.before.start;
        let ins_count = hunk.after.end - hunk.after.start;

        if del_count > 0 && ins_count > 0 {
            let left_lines: Vec<String> = (hunk.before.start..hunk.before.end)
                .map(|i| input.interner[input.before[i as usize]].to_string())
                .collect();
            let right_lines: Vec<String> = (hunk.after.start..hunk.after.end)
                .map(|i| input.interner[input.after[i as usize]].to_string())
                .collect();
            chunks.push(DiffChunk::Replace {
                left_lines,
                right_lines,
                left_start: hunk.before.start as usize,
                right_start: hunk.after.start as usize,
            });
        } else if del_count > 0 {
            let lines: Vec<String> = (hunk.before.start..hunk.before.end)
                .map(|i| input.interner[input.before[i as usize]].to_string())
                .collect();
            chunks.push(DiffChunk::Delete {
                lines,
                left_start: hunk.before.start as usize,
            });
        } else if ins_count > 0 {
            let lines: Vec<String> = (hunk.after.start..hunk.after.end)
                .map(|i| input.interner[input.after[i as usize]].to_string())
                .collect();
            chunks.push(DiffChunk::Insert {
                lines,
                right_start: hunk.after.start as usize,
            });
        }

        left_pos = hunk.before.end;
        right_pos = hunk.after.end;
    }

    // Trailing equal lines
    let total_before = input.before.len() as u32;
    if left_pos < total_before {
        let lines: Vec<String> = (left_pos..total_before)
            .map(|i| input.interner[input.before[i as usize]].to_string())
            .collect();
        chunks.push(DiffChunk::Equal {
            lines,
            left_start: left_pos as usize,
            right_start: right_pos as usize,
        });
    }

    Ok(FileDiff {
        left_path: left.to_path_buf(),
        right_path: right.to_path_buf(),
        chunks,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    fn temp_file(content: &str) -> NamedTempFile {
        let mut f = NamedTempFile::new().unwrap();
        write!(f, "{}", content).unwrap();
        f
    }

    #[test]
    fn identical_files_produce_only_equal_chunks() {
        let left = temp_file("line1\nline2\nline3\n");
        let right = temp_file("line1\nline2\nline3\n");

        let diff = diff_files(left.path(), right.path()).unwrap();
        assert!(diff
            .chunks
            .iter()
            .all(|c| matches!(c, DiffChunk::Equal { .. })));
        assert!(!diff.chunks.is_empty());
    }

    #[test]
    fn detects_inserted_lines() {
        let left = temp_file("line1\nline3\n");
        let right = temp_file("line1\nline2\nline3\n");

        let diff = diff_files(left.path(), right.path()).unwrap();
        let has_insert = diff
            .chunks
            .iter()
            .any(|c| matches!(c, DiffChunk::Insert { .. }));
        assert!(has_insert, "Expected at least one Insert chunk");
    }

    #[test]
    fn detects_deleted_lines() {
        let left = temp_file("line1\nline2\nline3\n");
        let right = temp_file("line1\nline3\n");

        let diff = diff_files(left.path(), right.path()).unwrap();
        let has_delete = diff
            .chunks
            .iter()
            .any(|c| matches!(c, DiffChunk::Delete { .. }));
        assert!(has_delete, "Expected at least one Delete chunk");
    }

    #[test]
    fn detects_replaced_lines() {
        let left = temp_file("line1\nold_line\nline3\n");
        let right = temp_file("line1\nnew_line\nline3\n");

        let diff = diff_files(left.path(), right.path()).unwrap();
        let has_replace = diff
            .chunks
            .iter()
            .any(|c| matches!(c, DiffChunk::Replace { .. }));
        assert!(has_replace, "Expected at least one Replace chunk");
    }

    #[test]
    fn equal_chunk_contains_correct_lines() {
        let left = temp_file("aaa\nbbb\nccc\n");
        let right = temp_file("aaa\nbbb\nccc\n");

        let diff = diff_files(left.path(), right.path()).unwrap();
        assert_eq!(diff.chunks.len(), 1);
        match &diff.chunks[0] {
            DiffChunk::Equal {
                lines,
                left_start,
                right_start,
            } => {
                assert_eq!(*left_start, 0);
                assert_eq!(*right_start, 0);
                assert_eq!(lines.len(), 3);
                assert!(lines[0].starts_with("aaa"));
                assert!(lines[1].starts_with("bbb"));
                assert!(lines[2].starts_with("ccc"));
            }
            other => panic!("Expected Equal chunk, got {:?}", other),
        }
    }

    #[test]
    fn replace_chunk_has_correct_content_and_positions() {
        let left = temp_file("keep\nold1\nold2\nkeep\n");
        let right = temp_file("keep\nnew1\nnew2\nkeep\n");

        let diff = diff_files(left.path(), right.path()).unwrap();
        // Should be: Equal("keep"), Replace(old1+old2 → new1+new2), Equal("keep")
        assert_eq!(diff.chunks.len(), 3);

        match &diff.chunks[0] {
            DiffChunk::Equal {
                lines,
                left_start,
                right_start,
            } => {
                assert_eq!(*left_start, 0);
                assert_eq!(*right_start, 0);
                assert!(lines[0].starts_with("keep"));
            }
            other => panic!("Expected Equal, got {:?}", other),
        }

        match &diff.chunks[1] {
            DiffChunk::Replace {
                left_lines,
                right_lines,
                left_start,
                right_start,
            } => {
                assert_eq!(*left_start, 1);
                assert_eq!(*right_start, 1);
                assert_eq!(left_lines.len(), 2);
                assert_eq!(right_lines.len(), 2);
                assert!(left_lines[0].starts_with("old1"));
                assert!(left_lines[1].starts_with("old2"));
                assert!(right_lines[0].starts_with("new1"));
                assert!(right_lines[1].starts_with("new2"));
            }
            other => panic!("Expected Replace, got {:?}", other),
        }

        match &diff.chunks[2] {
            DiffChunk::Equal {
                lines,
                left_start,
                right_start,
            } => {
                assert_eq!(*left_start, 3);
                assert_eq!(*right_start, 3);
                assert!(lines[0].starts_with("keep"));
            }
            other => panic!("Expected Equal, got {:?}", other),
        }
    }

    #[test]
    fn insert_chunk_has_correct_content_and_position() {
        let left = temp_file("aaa\nccc\n");
        let right = temp_file("aaa\nbbb\nccc\n");

        let diff = diff_files(left.path(), right.path()).unwrap();
        let insert = diff.chunks.iter().find_map(|c| match c {
            DiffChunk::Insert { lines, right_start } => Some((lines, *right_start)),
            _ => None,
        });
        let (lines, right_start) = insert.expect("Expected an Insert chunk");
        assert_eq!(right_start, 1);
        assert_eq!(lines.len(), 1);
        assert!(lines[0].starts_with("bbb"));
    }

    #[test]
    fn delete_chunk_has_correct_content_and_position() {
        let left = temp_file("aaa\nbbb\nccc\n");
        let right = temp_file("aaa\nccc\n");

        let diff = diff_files(left.path(), right.path()).unwrap();
        let delete = diff.chunks.iter().find_map(|c| match c {
            DiffChunk::Delete { lines, left_start } => Some((lines, *left_start)),
            _ => None,
        });
        let (lines, left_start) = delete.expect("Expected a Delete chunk");
        assert_eq!(left_start, 1);
        assert_eq!(lines.len(), 1);
        assert!(lines[0].starts_with("bbb"));
    }

    #[test]
    fn line_numbers_are_consistent_across_chunks() {
        let left = temp_file("a\nb\nc\nd\ne\n");
        let right = temp_file("a\nX\nc\nd\nY\n");

        let diff = diff_files(left.path(), right.path()).unwrap();

        // Walk all chunks and verify line numbers are continuous
        let mut expected_left = 0;
        let mut expected_right = 0;
        for chunk in &diff.chunks {
            match chunk {
                DiffChunk::Equal {
                    lines,
                    left_start,
                    right_start,
                } => {
                    assert_eq!(*left_start, expected_left, "Equal left_start mismatch");
                    assert_eq!(*right_start, expected_right, "Equal right_start mismatch");
                    expected_left += lines.len();
                    expected_right += lines.len();
                }
                DiffChunk::Replace {
                    left_lines,
                    right_lines,
                    left_start,
                    right_start,
                } => {
                    assert_eq!(*left_start, expected_left, "Replace left_start mismatch");
                    assert_eq!(*right_start, expected_right, "Replace right_start mismatch");
                    expected_left += left_lines.len();
                    expected_right += right_lines.len();
                }
                DiffChunk::Delete { lines, left_start } => {
                    assert_eq!(*left_start, expected_left, "Delete left_start mismatch");
                    expected_left += lines.len();
                }
                DiffChunk::Insert { lines, right_start } => {
                    assert_eq!(*right_start, expected_right, "Insert right_start mismatch");
                    expected_right += lines.len();
                }
            }
        }
    }

    #[test]
    fn empty_files_produce_no_chunks() {
        let left = temp_file("");
        let right = temp_file("");

        let diff = diff_files(left.path(), right.path()).unwrap();
        assert!(diff.chunks.is_empty());
    }

    #[test]
    fn left_empty_right_nonempty() {
        let left = temp_file("");
        let right = temp_file("hello\nworld\n");

        let diff = diff_files(left.path(), right.path()).unwrap();
        assert!(diff
            .chunks
            .iter()
            .all(|c| matches!(c, DiffChunk::Insert { .. })));
        let total_lines: usize = diff
            .chunks
            .iter()
            .map(|c| match c {
                DiffChunk::Insert { lines, .. } => lines.len(),
                _ => 0,
            })
            .sum();
        assert_eq!(total_lines, 2);
    }
}
