//! Project file index for the `@file` picker. Built once at startup
//! (the binary walks the tree; `update()` is pure and cannot do I/O)
//! and filtered with `nucleo` as the user types.

use std::path::Path;
use std::sync::Arc;

use nucleo::pattern::{CaseMatching, Normalization, Pattern};
use nucleo::{Config, Matcher};

/// An immutable list of project-relative file paths, shareable across the
/// `AppState` cheaply via `Arc`.
#[derive(Debug, Default, Clone)]
pub struct FileIndex {
    paths: Arc<Vec<String>>,
}

impl FileIndex {
    /// Walk `root` (gitignore-aware) and collect file paths relative to it.
    /// Capped at `MAX` entries so a huge monorepo can't blow up memory.
    pub fn build(root: &Path) -> Self {
        const MAX: usize = 20_000;
        let mut paths: Vec<String> = Vec::new();
        for entry in ignore::WalkBuilder::new(root).build().flatten() {
            if paths.len() >= MAX {
                break;
            }
            if entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
                let p = entry.path();
                let rel = p.strip_prefix(root).unwrap_or(p);
                paths.push(rel.display().to_string());
            }
        }
        paths.sort();
        Self {
            paths: Arc::new(paths),
        }
    }

    /// For tests / seeding from an explicit list.
    pub fn from_paths(paths: Vec<String>) -> Self {
        Self {
            paths: Arc::new(paths),
        }
    }

    pub fn len(&self) -> usize {
        self.paths.len()
    }

    pub fn is_empty(&self) -> bool {
        self.paths.is_empty()
    }

    /// Fuzzy-rank the index against `query`. Empty query → first `limit`
    /// paths in sorted order. Returns best matches first, capped at `limit`.
    pub fn filter(&self, query: &str, limit: usize) -> Vec<String> {
        if query.is_empty() {
            return self.paths.iter().take(limit).cloned().collect();
        }
        let mut matcher = Matcher::new(Config::DEFAULT.match_paths());
        let pattern = Pattern::parse(query, CaseMatching::Smart, Normalization::Smart);
        let mut scored: Vec<(u32, &String)> = self
            .paths
            .iter()
            .filter_map(|p| {
                let mut buf = Vec::new();
                let haystack = nucleo::Utf32Str::new(p, &mut buf);
                pattern.score(haystack, &mut matcher).map(|s| (s, p))
            })
            .collect();
        scored.sort_by_key(|(score, _)| std::cmp::Reverse(*score));
        scored
            .into_iter()
            .take(limit)
            .map(|(_, p)| p.clone())
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn idx() -> FileIndex {
        FileIndex::from_paths(vec![
            "src/main.rs".into(),
            "src/lib.rs".into(),
            "crates/capo-tui/src/update.rs".into(),
            "README.md".into(),
        ])
    }

    #[test]
    fn empty_query_returns_prefix_of_index() {
        let got = idx().filter("", 2);
        assert_eq!(got.len(), 2);
    }

    #[test]
    fn fuzzy_matches_subsequence() {
        let got = idx().filter("updatez", 10);
        assert!(got.is_empty() || !got.iter().any(|p| p.contains("update")));
        // "update" (a real subsequence) ranks update.rs first:
        let got = idx().filter("update", 10);
        assert_eq!(
            got.first().map(String::as_str),
            Some("crates/capo-tui/src/update.rs")
        );
    }

    #[test]
    fn ranks_main_for_main_query() {
        let got = idx().filter("main", 10);
        assert_eq!(got.first().map(String::as_str), Some("src/main.rs"));
    }

    #[test]
    fn limit_caps_results() {
        let got = idx().filter("", 100);
        assert_eq!(got.len(), 4);
    }
}
