//! Priority-based graph pruning for large codebases.
//!
//! Classifies files into priority levels to control indexing depth:
//! - **Critical** (~5%): full parse + graph + embeddings
//! - **Important** (~15%): full parse + graph, lazy embeddings
//! - **Reference** (~60%): metadata-only graph node
//! - **Archive** (~20%): skip unless queried

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

// ---------------------------------------------------------------------------
// Priority & IndexingMode
// ---------------------------------------------------------------------------

/// Priority level assigned to a file, controls indexing depth.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Priority {
    Critical = 0,
    Important = 1,
    Reference = 2,
    Archive = 3,
}

impl Priority {
    pub fn from_score(score: f32) -> Self {
        match score {
            s if s >= 0.75 => Priority::Critical,
            s if s >= 0.50 => Priority::Important,
            s if s >= 0.25 => Priority::Reference,
            _ => Priority::Archive,
        }
    }

    pub fn as_i32(self) -> i32 {
        self as i32
    }

    pub fn from_i32(v: i32) -> Self {
        match v {
            0 => Priority::Critical,
            1 => Priority::Important,
            2 => Priority::Reference,
            _ => Priority::Archive,
        }
    }

    /// Human-readable label.
    pub fn label(self) -> &'static str {
        match self {
            Priority::Critical => "Critical",
            Priority::Important => "Important",
            Priority::Reference => "Reference",
            Priority::Archive => "Archive",
        }
    }
}

/// How deeply a file should be indexed, derived from its priority.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndexingMode {
    /// Full parse → KuzuGraph (all entities) + LanceDb embedding.
    Full,
    /// Full parse → KuzuGraph (all entities), no embedding yet.
    GraphOnly,
    /// Metadata-only graph node (path + language).
    MetadataOnly,
    /// Skip entirely; index on-demand if queried.
    Skip,
}

impl IndexingMode {
    pub fn from_priority(priority: Priority) -> Self {
        match priority {
            Priority::Critical => IndexingMode::Full,
            Priority::Important => IndexingMode::GraphOnly,
            Priority::Reference => IndexingMode::MetadataOnly,
            Priority::Archive => IndexingMode::Skip,
        }
    }
}

// ---------------------------------------------------------------------------
// PriorityContext — ambient data heuristics can query
// ---------------------------------------------------------------------------

/// Contextual data available to heuristics during priority calculation.
pub struct PriorityContext {
    /// Number of git commits per file in the last N days.
    pub git_change_counts: HashMap<PathBuf, u32>,
    /// Workspace root (for relative path computation).
    pub workspace_root: PathBuf,
}

impl PriorityContext {
    pub fn new(workspace_root: PathBuf) -> Self {
        Self {
            git_change_counts: HashMap::new(),
            workspace_root,
        }
    }
}

// ---------------------------------------------------------------------------
// Heuristic trait
// ---------------------------------------------------------------------------

/// Scores a file from 0.0 (unimportant) to 1.0 (critical).
pub trait PriorityHeuristic: Send + Sync {
    fn score(&self, path: &Path, size_bytes: u64, ctx: &PriorityContext) -> f32;
    fn weight(&self) -> f32 {
        1.0
    }
}

// ---------------------------------------------------------------------------
// Concrete heuristics
// ---------------------------------------------------------------------------

/// Scores based on path patterns (src/core/ = high, vendor/ = low).
pub struct PathBasedHeuristic;

impl PriorityHeuristic for PathBasedHeuristic {
    fn score(&self, path: &Path, _size_bytes: u64, ctx: &PriorityContext) -> f32 {
        let rel = path
            .strip_prefix(&ctx.workspace_root)
            .unwrap_or(path)
            .to_string_lossy();
        let r = rel.as_ref();

        // High-priority paths
        if r.contains("src/core/") || r.contains("src/main") || r.contains("src/lib") {
            return 1.0;
        }
        if r.contains("src/api/") || r.contains("src/server/") || r.contains("src/app/") {
            return 0.9;
        }
        if r.starts_with("src/") || r.starts_with("lib/") || r.starts_with("app/") {
            return 0.7;
        }
        if r.starts_with("tests/") || r.starts_with("test/") || r.contains("/tests/") {
            return 0.5;
        }

        // Low-priority paths
        if r.contains("node_modules/")
            || r.contains("vendor/")
            || r.contains(".git/")
            || r.contains("target/")
        {
            return 0.05;
        }
        if r.contains("deprecated/") || r.contains("archive/") || r.contains("legacy/") {
            return 0.1;
        }
        if r.contains("dist/") || r.contains("build/") || r.contains("out/") {
            return 0.1;
        }
        if r.contains("docs/") || r.contains("documentation/") {
            return 0.3;
        }
        if r.contains("examples/") || r.contains("samples/") {
            return 0.4;
        }

        0.5 // default
    }
}

/// Smaller files → higher priority (easier to index fully).
pub struct FileSizeHeuristic;

impl PriorityHeuristic for FileSizeHeuristic {
    fn score(&self, _path: &Path, size_bytes: u64, _ctx: &PriorityContext) -> f32 {
        let kb = size_bytes / 1024;
        match kb {
            0..=10 => 1.0,
            11..=50 => 0.8,
            51..=200 => 0.6,
            201..=500 => 0.4,
            _ => 0.2,
        }
    }

    fn weight(&self) -> f32 {
        0.5 // lower weight — size matters less than location/changes
    }
}

/// Recently-changed files get higher priority.
pub struct ChangeFrequencyHeuristic;

impl PriorityHeuristic for ChangeFrequencyHeuristic {
    fn score(&self, path: &Path, _size_bytes: u64, ctx: &PriorityContext) -> f32 {
        let count = ctx.git_change_counts.get(path).copied().unwrap_or(0);
        // Normalize: 0 commits = 0.0, 20+ commits = 1.0
        (count as f32 / 20.0).min(1.0)
    }

    fn weight(&self) -> f32 {
        1.5 // high weight — active code deserves priority
    }
}

// ---------------------------------------------------------------------------
// GraphPruner
// ---------------------------------------------------------------------------

/// Combines multiple heuristics to produce a final priority for each file.
pub struct GraphPruner {
    heuristics: Vec<Box<dyn PriorityHeuristic>>,
}

impl GraphPruner {
    /// Create a pruner with the default set of heuristics.
    pub fn new() -> Self {
        Self {
            heuristics: vec![
                Box::new(PathBasedHeuristic),
                Box::new(FileSizeHeuristic),
                Box::new(ChangeFrequencyHeuristic),
            ],
        }
    }

    /// Compute the weighted average score for a file.
    pub fn score(&self, path: &Path, size_bytes: u64, ctx: &PriorityContext) -> f32 {
        if self.heuristics.is_empty() {
            return 0.5;
        }
        let (total_weight, weighted_sum) =
            self.heuristics.iter().fold((0.0_f32, 0.0_f32), |(tw, ws), h| {
                let w = h.weight();
                let s = h.score(path, size_bytes, ctx);
                (tw + w, ws + w * s)
            });
        if total_weight == 0.0 {
            return 0.5;
        }
        weighted_sum / total_weight
    }

    /// Determine the priority level for a file.
    pub fn priority(&self, path: &Path, size_bytes: u64, ctx: &PriorityContext) -> Priority {
        Priority::from_score(self.score(path, size_bytes, ctx))
    }

    /// Determine how deeply to index a file.
    pub fn indexing_mode(
        &self,
        path: &Path,
        size_bytes: u64,
        ctx: &PriorityContext,
    ) -> IndexingMode {
        IndexingMode::from_priority(self.priority(path, size_bytes, ctx))
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    fn ctx(workspace: &str) -> PriorityContext {
        PriorityContext::new(PathBuf::from(workspace))
    }

    fn ctx_with_changes(workspace: &str, changes: Vec<(&str, u32)>) -> PriorityContext {
        let mut c = PriorityContext::new(PathBuf::from(workspace));
        for (p, count) in changes {
            c.git_change_counts.insert(PathBuf::from(p), count);
        }
        c
    }

    #[test]
    fn test_priority_from_score() {
        assert_eq!(Priority::from_score(1.0), Priority::Critical);
        assert_eq!(Priority::from_score(0.75), Priority::Critical);
        assert_eq!(Priority::from_score(0.6), Priority::Important);
        assert_eq!(Priority::from_score(0.5), Priority::Important);
        assert_eq!(Priority::from_score(0.3), Priority::Reference);
        assert_eq!(Priority::from_score(0.25), Priority::Reference);
        assert_eq!(Priority::from_score(0.1), Priority::Archive);
        assert_eq!(Priority::from_score(0.0), Priority::Archive);
    }

    #[test]
    fn test_priority_roundtrip() {
        for p in [
            Priority::Critical,
            Priority::Important,
            Priority::Reference,
            Priority::Archive,
        ] {
            assert_eq!(Priority::from_i32(p.as_i32()), p);
        }
    }

    #[test]
    fn test_indexing_mode_from_priority() {
        assert_eq!(
            IndexingMode::from_priority(Priority::Critical),
            IndexingMode::Full
        );
        assert_eq!(
            IndexingMode::from_priority(Priority::Important),
            IndexingMode::GraphOnly
        );
        assert_eq!(
            IndexingMode::from_priority(Priority::Reference),
            IndexingMode::MetadataOnly
        );
        assert_eq!(
            IndexingMode::from_priority(Priority::Archive),
            IndexingMode::Skip
        );
    }

    #[test]
    fn test_path_heuristic_core_paths() {
        let c = ctx("/project");
        let h = PathBasedHeuristic;
        assert_eq!(h.score(Path::new("/project/src/core/auth.rs"), 0, &c), 1.0);
        assert_eq!(h.score(Path::new("/project/src/api/routes.rs"), 0, &c), 0.9);
        assert_eq!(h.score(Path::new("/project/src/utils.rs"), 0, &c), 0.7);
    }

    #[test]
    fn test_path_heuristic_low_priority_paths() {
        let c = ctx("/project");
        let h = PathBasedHeuristic;
        assert_eq!(
            h.score(Path::new("/project/node_modules/foo/index.js"), 0, &c),
            0.05
        );
        assert_eq!(
            h.score(Path::new("/project/vendor/lib.py"), 0, &c),
            0.05
        );
        assert_eq!(
            h.score(Path::new("/project/deprecated/old.rs"), 0, &c),
            0.1
        );
    }

    #[test]
    fn test_file_size_heuristic() {
        let c = ctx("/project");
        let h = FileSizeHeuristic;
        assert_eq!(h.score(Path::new("a.rs"), 5_000, &c), 1.0); // 4KB
        assert_eq!(h.score(Path::new("b.rs"), 30_000, &c), 0.8); // 29KB
        assert_eq!(h.score(Path::new("c.rs"), 100_000, &c), 0.6); // 97KB
        assert_eq!(h.score(Path::new("d.rs"), 300_000, &c), 0.4); // 292KB
        assert_eq!(h.score(Path::new("e.rs"), 2_000_000, &c), 0.2); // 1.9MB
    }

    #[test]
    fn test_change_frequency_heuristic() {
        let c = ctx_with_changes(
            "/project",
            vec![
                ("/project/src/hot.rs", 25),
                ("/project/src/warm.rs", 10),
                ("/project/src/cold.rs", 2),
            ],
        );
        let h = ChangeFrequencyHeuristic;
        assert_eq!(h.score(Path::new("/project/src/hot.rs"), 0, &c), 1.0);
        assert_eq!(h.score(Path::new("/project/src/warm.rs"), 0, &c), 0.5);
        assert_eq!(h.score(Path::new("/project/src/cold.rs"), 0, &c), 0.1);
        assert_eq!(h.score(Path::new("/project/src/untouched.rs"), 0, &c), 0.0);
    }

    #[test]
    fn test_graph_pruner_combined() {
        let pruner = GraphPruner::new();
        let c = ctx_with_changes(
            "/project",
            vec![("/project/src/core/auth.rs", 15)],
        );

        // Core file, small, actively changed → Critical
        let p = pruner.priority(Path::new("/project/src/core/auth.rs"), 5_000, &c);
        assert_eq!(p, Priority::Critical);

        // Vendor file, large, never changed → Archive
        let p = pruner.priority(
            Path::new("/project/node_modules/big-lib/index.js"),
            2_000_000,
            &c,
        );
        assert_eq!(p, Priority::Archive);
    }

    #[test]
    fn test_graph_pruner_empty_heuristics() {
        let pruner = GraphPruner {
            heuristics: vec![],
        };
        let c = ctx("/project");
        assert_eq!(pruner.score(Path::new("a.rs"), 0, &c), 0.5);
    }

    #[test]
    fn test_priority_ordering() {
        assert!(Priority::Critical < Priority::Important);
        assert!(Priority::Important < Priority::Reference);
        assert!(Priority::Reference < Priority::Archive);
    }

    #[test]
    fn test_priority_labels() {
        assert_eq!(Priority::Critical.label(), "Critical");
        assert_eq!(Priority::Archive.label(), "Archive");
    }
}
