// SPDX-License-Identifier: Apache-2.0
//! Worktree comparison logic.

use std::{collections::HashMap, fs, path::Path};

use super::{worktree_ignore::should_ignore, worktree_types::WorktreeStatus};
use crate::{
    error::Result,
    object::{Blob, EntryType, Tree, TreeEntry},
    store::ObjectStore,
};

/// Compare worktree against a tree.
pub fn compare_worktree<S: ObjectStore + ?Sized>(
    store: &S,
    root: &Path,
    tree: &Tree,
    ignore_patterns: &[String],
) -> Result<WorktreeStatus> {
    let mut status = WorktreeStatus::default();
    compare_worktree_recursive(store, root, root, Some(tree), ignore_patterns, &mut status)?;

    status.modified.sort();
    status.added.sort();
    status.deleted.sort();

    Ok(status)
}

fn compare_worktree_recursive<S: ObjectStore + ?Sized>(
    store: &S,
    base: &Path,
    dir: &Path,
    tree: Option<&Tree>,
    ignore_patterns: &[String],
    status: &mut WorktreeStatus,
) -> Result<()> {
    let tree_entries: HashMap<&str, &TreeEntry> = tree
        .map(|t| t.entries().iter().map(|e| (e.name(), e)).collect())
        .unwrap_or_default();

    let mut seen_entries: std::collections::HashSet<&str> = std::collections::HashSet::new();

    if dir.exists() {
        for entry in fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();
            let name = match path.file_name().and_then(|n| n.to_str()) {
                Some(n) => n,
                None => continue,
            };

            let rel_path = path.strip_prefix(base).unwrap_or(&path);

            if should_ignore(rel_path, ignore_patterns) {
                continue;
            }

            if let Some((tree_name, _)) = tree_entries.get_key_value(name) {
                seen_entries.insert(*tree_name);
            }

            if path.is_symlink() {
                let target = fs::read_link(&path)?;
                let blob = Blob::new(crate::util::symlink_target_bytes(&target));
                let hash = blob.hash();
                match tree_entries.get(&name) {
                    Some(tree_entry) if tree_entry.entry_type() == EntryType::Symlink => {
                        if Some(hash) != tree_entry.symlink_hash() {
                            status.modified.push(rel_path.to_path_buf());
                        }
                    }
                    Some(_) => {
                        status.modified.push(rel_path.to_path_buf());
                    }
                    None => {
                        status.added.push(rel_path.to_path_buf());
                    }
                }
            } else {
                let metadata = path.metadata()?;

                if metadata.is_file() {
                    match tree_entries.get(&name) {
                        Some(tree_entry) if tree_entry.is_blob() => {
                            let content = fs::read(&path)?;
                            let blob = Blob::new(content);
                            let hash = blob.hash();

                            if Some(hash) != tree_entry.blob_hash() {
                                status.modified.push(rel_path.to_path_buf());
                            }
                        }
                        _ => {
                            status.added.push(rel_path.to_path_buf());
                        }
                    }
                } else if metadata.is_dir() {
                    let subtree = match tree_entries.get(&name) {
                        Some(tree_entry) if tree_entry.is_tree() => tree_entry
                            .tree_hash()
                            .map(|hash| store.get_tree(&hash))
                            .transpose()?
                            .flatten(),
                        _ => None,
                    };

                    compare_worktree_recursive(
                        store,
                        base,
                        &path,
                        subtree.as_ref(),
                        ignore_patterns,
                        status,
                    )?;
                }
            }
        }
    }

    for (name, entry) in &tree_entries {
        if !seen_entries.contains(name) {
            let rel_path = dir.strip_prefix(base).unwrap_or(dir).join(name);

            if entry.entry_type() == EntryType::Blob || entry.entry_type() == EntryType::Gitlink {
                status.deleted.push(rel_path);
            } else if let Some(tree_hash) = entry.tree_hash()
                && let Some(subtree) = store.get_tree(&tree_hash)?
            {
                mark_all_deleted(store, &rel_path, &subtree, status)?;
            }
        }
    }

    Ok(())
}

fn mark_all_deleted<S: ObjectStore + ?Sized>(
    store: &S,
    prefix: &Path,
    tree: &Tree,
    status: &mut WorktreeStatus,
) -> Result<()> {
    for entry in tree.entries() {
        let path = prefix.join(entry.name());

        match entry.entry_type() {
            EntryType::Blob => {
                status.deleted.push(path);
            }
            EntryType::Tree => {
                if let Some(tree_hash) = entry.tree_hash()
                    && let Some(subtree) = store.get_tree(&tree_hash)?
                {
                    mark_all_deleted(store, &path, &subtree, status)?;
                }
            }
            EntryType::Symlink | EntryType::Gitlink => {
                status.deleted.push(path);
            }
            // Native child-spool edge: not a filesystem leaf, so there is
            // nothing on disk to report as deleted.
            EntryType::Spoollink => {}
        }
    }
    Ok(())
}
