use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use walkdir::WalkDir;

use crate::hash::hash_file;
use crate::ignore::should_ignore;
use crate::types::{CompareResult, DirTree, EntryStatus, TreeEntry};

/// Callback type for progress reporting during comparison.
type ProgressFn<'a> = Option<&'a dyn Fn(&str, &EntryStatus)>;

/// Represents a file entry found during walking, tracking which side(s) it exists on.
#[derive(Debug)]
enum Side {
    Left,
    Right,
    Both,
}

/// Collect all relative file paths from a directory into a BTreeMap.
/// Keys are relative paths (as strings), values are `Side`.
fn collect_entries(root: &Path) -> io::Result<BTreeMap<PathBuf, bool>> {
    let mut entries = BTreeMap::new();
    for entry in WalkDir::new(root) {
        let entry = entry.map_err(io::Error::other)?;
        let rel = entry.path().strip_prefix(root).map_err(io::Error::other)?;
        if should_ignore(rel) {
            continue;
        }
        if entry.file_type().is_file() {
            entries.insert(rel.to_path_buf(), true);
        }
    }
    Ok(entries)
}

/// Compare a file that exists in both trees: first by size, then by hash.
fn compare_file(left_path: &Path, right_path: &Path) -> io::Result<EntryStatus> {
    let left_meta = fs::metadata(left_path)?;
    let right_meta = fs::metadata(right_path)?;

    if left_meta.len() != right_meta.len() {
        return Ok(EntryStatus::Modified);
    }

    let left_hash = hash_file(left_path)?;
    let right_hash = hash_file(right_path)?;

    if left_hash == right_hash {
        Ok(EntryStatus::Identical)
    } else {
        Ok(EntryStatus::Modified)
    }
}

/// Determine directory status from its children's statuses.
fn aggregate_status(children: &[TreeEntry]) -> EntryStatus {
    let mut all_identical = true;
    let mut all_left_only = true;
    let mut all_right_only = true;

    for child in children {
        let status = match child {
            TreeEntry::File { status, .. } => status,
            TreeEntry::Dir { status, .. } => status,
        };
        match status {
            EntryStatus::Identical => {
                all_left_only = false;
                all_right_only = false;
            }
            EntryStatus::Modified | EntryStatus::TypeMismatch { .. } => {
                all_identical = false;
                all_left_only = false;
                all_right_only = false;
            }
            EntryStatus::LeftOnly => {
                all_identical = false;
                all_right_only = false;
            }
            EntryStatus::RightOnly => {
                all_identical = false;
                all_left_only = false;
            }
        }
    }

    if all_identical {
        EntryStatus::Identical
    } else if all_left_only {
        EntryStatus::LeftOnly
    } else if all_right_only {
        EntryStatus::RightOnly
    } else {
        EntryStatus::Modified
    }
}

/// Intermediate tree node used during construction.
#[derive(Debug)]
enum MergedNode {
    File {
        side: Side,
    },
    Dir {
        children: BTreeMap<String, MergedNode>,
    },
    /// Left has a file and right has a dir (or vice versa) with the same name.
    TypeMismatch {
        left_is_dir: bool,
    },
}

/// Insert a relative path into the merged tree.
fn insert_path(tree: &mut BTreeMap<String, MergedNode>, rel: &Path, side: Side) {
    let components: Vec<&str> = rel
        .components()
        .map(|c| c.as_os_str().to_str().unwrap())
        .collect();

    insert_components(tree, &components, side);
}

fn insert_components(tree: &mut BTreeMap<String, MergedNode>, components: &[&str], side: Side) {
    if components.is_empty() {
        return;
    }

    let name = components[0].to_string();

    if components.len() == 1 {
        // File node
        match tree.get(&name) {
            Some(MergedNode::File { .. }) => {
                // Already seen from the other side
                if let Some(MergedNode::File {
                    side: existing_side,
                }) = tree.get_mut(&name)
                {
                    *existing_side = Side::Both;
                }
            }
            Some(MergedNode::Dir { .. }) => {
                // We're inserting a file, existing is a dir from the other side.
                // If side is Left, left=file, right=dir → left_is_dir=false
                // If side is Right, right=file, left=dir → left_is_dir=true
                let left_is_dir = matches!(side, Side::Right);
                tree.insert(name, MergedNode::TypeMismatch { left_is_dir });
            }
            _ => {
                tree.insert(name, MergedNode::File { side });
            }
        }
    } else {
        // Directory node
        match tree.get(&name) {
            Some(MergedNode::File { .. }) => {
                // We're inserting a dir, existing is a file from the other side.
                // If side is Left, left=dir, right=file → left_is_dir=true
                // If side is Right, right=dir, left=file → left_is_dir=false
                let left_is_dir = matches!(side, Side::Left);
                tree.insert(name, MergedNode::TypeMismatch { left_is_dir });
            }
            _ => {
                let dir_node = tree.entry(name).or_insert_with(|| MergedNode::Dir {
                    children: BTreeMap::new(),
                });
                if let MergedNode::Dir { children } = dir_node {
                    insert_components(children, &components[1..], side);
                }
            }
        }
    }
}

/// Convert the merged tree into TreeEntry vec, performing file comparisons.
fn build_tree_entries(
    merged: &BTreeMap<String, MergedNode>,
    left_root: &Path,
    right_root: &Path,
    rel_prefix: &Path,
    counts: &mut Counts,
    skip_hash: bool,
    progress: ProgressFn<'_>,
) -> io::Result<Vec<TreeEntry>> {
    let mut entries = Vec::new();

    for (name, node) in merged {
        let rel_path = rel_prefix.join(name);
        match node {
            MergedNode::File { side } => {
                let status = match side {
                    Side::Left => {
                        counts.left_only += 1;
                        counts.total += 1;
                        EntryStatus::LeftOnly
                    }
                    Side::Right => {
                        counts.right_only += 1;
                        counts.total += 1;
                        EntryStatus::RightOnly
                    }
                    Side::Both => {
                        counts.total += 1;
                        if skip_hash {
                            EntryStatus::Identical
                        } else {
                            let left_path = left_root.join(&rel_path);
                            let right_path = right_root.join(&rel_path);
                            let s = compare_file(&left_path, &right_path)?;
                            if s == EntryStatus::Modified {
                                counts.modified += 1;
                            }
                            if let Some(cb) = &progress {
                                cb(&rel_path.to_string_lossy(), &s);
                            }
                            s
                        }
                    }
                };
                entries.push(TreeEntry::File {
                    name: name.clone(),
                    status,
                });
            }
            MergedNode::Dir { children } => {
                let child_entries = build_tree_entries(
                    children, left_root, right_root, &rel_path, counts, skip_hash, progress,
                )?;
                let status = aggregate_status(&child_entries);
                entries.push(TreeEntry::Dir {
                    name: name.clone(),
                    status,
                    children: child_entries,
                });
            }
            MergedNode::TypeMismatch { left_is_dir } => {
                counts.total += 1;
                counts.modified += 1;
                entries.push(TreeEntry::File {
                    name: name.clone(),
                    status: EntryStatus::TypeMismatch {
                        left_is_dir: *left_is_dir,
                    },
                });
            }
        }
    }

    Ok(entries)
}

#[derive(Debug, Default)]
struct Counts {
    total: usize,
    modified: usize,
    left_only: usize,
    right_only: usize,
}

/// Compare two directories. If `progress` is provided, it's called with
/// each relative path as files are being compared.
pub fn compare_dirs(left: &Path, right: &Path) -> io::Result<CompareResult> {
    compare_dirs_with_progress(left, right, None)
}

/// Like `compare_dirs` but calls `progress` with (relative_path, status) for each
/// file that exists on both sides, as it's compared. This allows the caller to
/// incrementally update the UI.
pub fn compare_dirs_with_progress(
    left: &Path,
    right: &Path,
    progress: ProgressFn<'_>,
) -> io::Result<CompareResult> {
    let left = left.canonicalize()?;
    let right = right.canonicalize()?;

    // Collect all file relative paths from both sides
    let left_files = collect_entries(&left)?;
    let right_files = collect_entries(&right)?;

    // Build merged tree
    let mut merged: BTreeMap<String, MergedNode> = BTreeMap::new();

    for rel in left_files.keys() {
        insert_path(&mut merged, rel, Side::Left);
    }
    for rel in right_files.keys() {
        insert_path(&mut merged, rel, Side::Right);
    }

    // Convert merged tree to TreeEntry vec with comparisons
    let mut counts = Counts::default();
    let entries = build_tree_entries(
        &merged,
        &left,
        &right,
        &PathBuf::new(),
        &mut counts,
        false,
        progress,
    )?;

    let left_name = left
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| left.display().to_string());
    let right_name = right
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| right.display().to_string());

    let tree_name = format!("{} vs {}", left_name, right_name);

    Ok(CompareResult {
        left_root: left,
        right_root: right,
        tree: DirTree {
            name: tree_name,
            entries,
        },
        total_files: counts.total,
        modified_files: counts.modified,
        left_only_files: counts.left_only,
        right_only_files: counts.right_only,
    })
}

/// Like `compare_dirs` but skips file hashing − returns the tree structure
/// immediately with all "Both" files marked as Modified. Use this for a fast
/// initial display while the full comparison runs in the background.
pub fn walk_dirs(left: &Path, right: &Path) -> io::Result<CompareResult> {
    let left = left.canonicalize()?;
    let right = right.canonicalize()?;

    let left_files = collect_entries(&left)?;
    let right_files = collect_entries(&right)?;

    let mut merged: BTreeMap<String, MergedNode> = BTreeMap::new();
    for rel in left_files.keys() {
        insert_path(&mut merged, rel, Side::Left);
    }
    for rel in right_files.keys() {
        insert_path(&mut merged, rel, Side::Right);
    }

    let mut counts = Counts::default();
    let entries = build_tree_entries(
        &merged,
        &left,
        &right,
        &PathBuf::new(),
        &mut counts,
        true,
        None,
    )?;

    let left_name = left
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| left.display().to_string());
    let right_name = right
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| right.display().to_string());

    Ok(CompareResult {
        left_root: left,
        right_root: right,
        tree: DirTree {
            name: format!("{} vs {}", left_name, right_name),
            entries,
        },
        total_files: counts.total,
        modified_files: counts.modified,
        left_only_files: counts.left_only,
        right_only_files: counts.right_only,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    fn create_file(base: &Path, rel: &str, content: &str) {
        let path = base.join(rel);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).unwrap();
        }
        fs::write(&path, content).unwrap();
    }

    #[test]
    fn detects_identical_files() {
        let left = TempDir::new().unwrap();
        let right = TempDir::new().unwrap();
        create_file(left.path(), "a.txt", "hello");
        create_file(right.path(), "a.txt", "hello");

        let result = compare_dirs(left.path(), right.path()).unwrap();
        assert_eq!(result.total_files, 1);
        assert_eq!(result.modified_files, 0);
        assert_eq!(result.left_only_files, 0);
        assert_eq!(result.right_only_files, 0);
    }

    #[test]
    fn detects_modified_files() {
        let left = TempDir::new().unwrap();
        let right = TempDir::new().unwrap();
        create_file(left.path(), "a.txt", "hello");
        create_file(right.path(), "a.txt", "world");

        let result = compare_dirs(left.path(), right.path()).unwrap();
        assert_eq!(result.modified_files, 1);
    }

    #[test]
    fn detects_left_only_files() {
        let left = TempDir::new().unwrap();
        let right = TempDir::new().unwrap();
        create_file(left.path(), "only_left.txt", "data");

        let result = compare_dirs(left.path(), right.path()).unwrap();
        assert_eq!(result.left_only_files, 1);
        assert_eq!(result.right_only_files, 0);
    }

    #[test]
    fn detects_right_only_files() {
        let left = TempDir::new().unwrap();
        let right = TempDir::new().unwrap();
        create_file(right.path(), "only_right.txt", "data");

        let result = compare_dirs(left.path(), right.path()).unwrap();
        assert_eq!(result.right_only_files, 1);
        assert_eq!(result.left_only_files, 0);
    }

    #[test]
    fn counts_are_correct() {
        let left = TempDir::new().unwrap();
        let right = TempDir::new().unwrap();
        create_file(left.path(), "same.txt", "same");
        create_file(right.path(), "same.txt", "same");
        create_file(left.path(), "changed.txt", "old");
        create_file(right.path(), "changed.txt", "new");
        create_file(left.path(), "left.txt", "left");
        create_file(right.path(), "right.txt", "right");

        let result = compare_dirs(left.path(), right.path()).unwrap();
        assert_eq!(result.total_files, 4);
        assert_eq!(result.modified_files, 1);
        assert_eq!(result.left_only_files, 1);
        assert_eq!(result.right_only_files, 1);
    }

    #[test]
    fn handles_nested_directories() {
        let left = TempDir::new().unwrap();
        let right = TempDir::new().unwrap();
        create_file(left.path(), "sub/deep/file.txt", "old");
        create_file(right.path(), "sub/deep/file.txt", "new");

        let result = compare_dirs(left.path(), right.path()).unwrap();
        assert_eq!(result.modified_files, 1);

        // Check the tree structure has the nested dir marked as Modified
        let sub_entry = result.tree.entries.iter().find(|e| match e {
            TreeEntry::Dir { name, .. } => name == "sub",
            _ => false,
        });
        assert!(sub_entry.is_some());
        if let Some(TreeEntry::Dir { status, .. }) = sub_entry {
            assert_eq!(*status, EntryStatus::Modified);
        }
    }

    /// Helper to collect all file names recursively from a tree.
    fn collect_file_names(entries: &[TreeEntry]) -> Vec<String> {
        let mut names = Vec::new();
        for entry in entries {
            match entry {
                TreeEntry::File { name, .. } => names.push(name.clone()),
                TreeEntry::Dir { children, .. } => {
                    names.extend(collect_file_names(children));
                }
            }
        }
        names
    }

    #[test]
    fn ignored_files_excluded_from_compare_dirs() {
        let left = TempDir::new().unwrap();
        let right = TempDir::new().unwrap();

        // Normal files (should appear)
        create_file(left.path(), "hello.rs", "fn main() {}");
        create_file(right.path(), "hello.rs", "fn main() {}");
        create_file(left.path(), "lib.rs", "pub mod foo;");
        create_file(right.path(), "lib.rs", "pub mod foo;");

        // Ignored files (should NOT appear)
        create_file(left.path(), "file.swp", "swap data");
        create_file(right.path(), "file.swp", "swap data");
        create_file(left.path(), ".git/config", "[core]");
        create_file(right.path(), ".git/config", "[core]");
        create_file(left.path(), "backup~", "old stuff");
        create_file(right.path(), "backup~", "old stuff");

        let result = compare_dirs(left.path(), right.path()).unwrap();
        let names = collect_file_names(&result.tree.entries);

        // Normal files present
        assert!(names.contains(&"hello.rs".to_string()));
        assert!(names.contains(&"lib.rs".to_string()));

        // Ignored files absent
        assert!(!names.contains(&"file.swp".to_string()));
        assert!(!names.contains(&"config".to_string()));
        assert!(!names.contains(&"backup~".to_string()));

        // Only the two normal files counted
        assert_eq!(result.total_files, 2);
    }
}
