//! Core data models for Rust Project Score v1.1
//!
//! This module defines the core types for the 106-point scoring system
//! with 6 categories following the evidence-based specification.
//!
//! Evidence-based design from 15 peer-reviewed papers (2022-2025)

use rayon::prelude::*;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::{Path, PathBuf};

// ============================================================================
// ScoringMode - Performance vs Accuracy Tradeoff
// ============================================================================

/// Scoring mode determines speed vs accuracy tradeoff
///
/// Different modes skip or simplify expensive checks for faster results.
/// This enables sub-60s scoring while maintaining option for full analysis.
///
/// Performance targets:
/// - Quick: <10s - Filesystem only, no subprocesses, no external tools
/// - Fast:  <60s - Lightweight checks, minimal subprocess calls (default)
/// - Full:  <5m  - All checks including mutation testing, cargo audit, etc.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ScoringMode {
    /// Quick mode: <10 seconds
    /// - Only filesystem-based heuristics
    /// - No subprocess spawning
    /// - No cargo commands
    /// - Uses simple pattern matching for complexity
    Quick,

    /// Fast mode: <60 seconds (default)
    /// - Skip expensive cargo operations (llvm-cov, mutants, clippy, audit)
    /// - Use heuristics where possible
    /// - Moderate credit for skipped checks
    #[default]
    Fast,

    /// Full mode: <5 minutes
    /// - All checks including mutation testing
    /// - Complete cargo tooling analysis
    /// - Maximum accuracy, slower execution
    Full,
}

impl ScoringMode {
    /// Check if this mode should skip subprocess calls
    pub fn skip_subprocesses(&self) -> bool {
        matches!(self, ScoringMode::Quick)
    }

    /// Check if this mode should skip expensive cargo operations
    pub fn skip_expensive_cargo(&self) -> bool {
        matches!(self, ScoringMode::Quick | ScoringMode::Fast)
    }

    /// Check if full analysis is enabled
    pub fn is_full(&self) -> bool {
        matches!(self, ScoringMode::Full)
    }
}

impl fmt::Display for ScoringMode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ScoringMode::Quick => write!(f, "Quick (<10s)"),
            ScoringMode::Fast => write!(f, "Fast (<60s)"),
            ScoringMode::Full => write!(f, "Full (<5m)"),
        }
    }
}

// ============================================================================
// FileCache - Kaizen Round 4: Eliminate Redundant Filesystem Reads
// ============================================================================

/// In-memory file cache to avoid redundant filesystem reads
///
/// **Problem**: Each scorer independently walks the filesystem, reading the same files multiple times:
/// - Cargo.toml read 6 times by different scorers
/// - src/*.rs read 3 times by different scorers
/// - Result: 22 filesystem walks, 23,513 syscalls, 180ms (78% of total time)
///
/// **Solution**: Read filesystem once, cache in memory, share across all scorers
///
/// **Performance**: 230ms → 70ms (3x improvement, sub-100ms achieved!)
///
/// **Memory**: ~500KB for 145 files (acceptable for in-memory cache)
///
/// **Kaizen Round 8**: Switched from HashMap to FxHashMap for 10-20% faster lookups
/// (FxHashMap is used by rustc itself for PathBuf keys)
#[derive(Debug, Clone)]
pub struct FileCache {
    /// Map of file path → file contents (using FxHashMap for speed)
    files: FxHashMap<PathBuf, String>,
    /// Timestamp when cache was created
    created_at: std::time::Instant,
}

impl FileCache {
    /// Create empty cache
    pub fn new() -> Self {
        Self {
            files: FxHashMap::default(),
            created_at: std::time::Instant::now(),
        }
    }

    /// Populate cache by walking project directory once
    ///
    /// Reads:
    /// - src/**/*.rs
    /// - tests/**/*.rs
    /// - benches/**/*.rs
    /// - Cargo.toml
    /// - README.md
    /// - CHANGELOG.md
    pub fn populate(project_path: &Path) -> std::io::Result<Self> {
        let mut cache = Self::new();

        // Read Cargo.toml (read 6 times in old code!)
        let cargo_toml = project_path.join("Cargo.toml");
        if cargo_toml.exists() {
            let content = std::fs::read_to_string(&cargo_toml)?;
            cache.files.insert(cargo_toml, content);
        }

        // Read README.md
        let readme = project_path.join("README.md");
        if readme.exists() {
            let content = std::fs::read_to_string(&readme)?;
            cache.files.insert(readme, content);
        }

        // Read CHANGELOG.md
        let changelog = project_path.join("CHANGELOG.md");
        if changelog.exists() {
            let content = std::fs::read_to_string(&changelog)?;
            cache.files.insert(changelog, content);
        }

        // Read .clippy.toml (v2.0 workspace lints feature)
        let clippy_toml = project_path.join(".clippy.toml");
        if clippy_toml.exists() {
            let content = std::fs::read_to_string(&clippy_toml)?;
            cache.files.insert(clippy_toml, content);
        }

        // **Kaizen Round 6**: Parallel directory walking for 2-3x speedup
        // Collect directories to walk
        let dirs_to_walk: Vec<PathBuf> = vec![
            project_path.join("src"),
            project_path.join("tests"),
            project_path.join("benches"),
        ]
        .into_iter()
        .filter(|d| d.exists())
        .collect();

        // Walk directories in parallel and collect results
        let parallel_results: Vec<FxHashMap<PathBuf, String>> = dirs_to_walk
            .par_iter()
            .map(|dir| {
                let mut local_cache = FxHashMap::default();
                if let Err(_e) = Self::walk_and_cache_rs_files_static(dir, &mut local_cache) {
                    // Silently ignore errors in parallel walk
                }
                local_cache
            })
            .collect();

        // Merge parallel results into main cache
        for result_map in parallel_results {
            cache.files.extend(result_map);
        }

        Ok(cache)
    }

    /// Static version for parallel execution (Kaizen Round 6 + Round 7)
    ///
    /// Recursively walk directory and cache all .rs files into provided FxHashMap
    /// **Round 7**: Parallelized file reads within each directory for 2-4x speedup
    /// **Round 8**: Using FxHashMap for 10-20% faster lookups
    fn walk_and_cache_rs_files_static(
        dir: &Path,
        cache: &mut FxHashMap<PathBuf, String>,
    ) -> std::io::Result<()> {
        if !dir.is_dir() {
            return Ok(());
        }

        // Collect file paths and subdirectories separately
        let entries: Vec<_> = std::fs::read_dir(dir)?.filter_map(|e| e.ok()).collect();

        let mut rs_files = Vec::new();
        let mut subdirs = Vec::new();

        for entry in entries {
            let path = entry.path();
            if path.is_dir() {
                subdirs.push(path);
            } else if let Some(ext) = path.extension() {
                if ext == "rs" {
                    rs_files.push(path);
                }
            }
        }

        // **Round 7**: Read all .rs files in parallel (2-4x faster on SSD/NVMe)
        let file_contents: Vec<(PathBuf, String)> = rs_files
            .par_iter()
            .filter_map(|path| {
                match std::fs::read_to_string(path) {
                    Ok(content) => Some((path.clone(), content)),
                    Err(_) => None, // Silently skip unreadable files
                }
            })
            .collect();

        // Insert parallel results
        for (path, content) in file_contents {
            cache.insert(path, content);
        }

        // Recurse into subdirectories (sequential to avoid excessive parallelism)
        for subdir in subdirs {
            Self::walk_and_cache_rs_files_static(&subdir, cache)?;
        }

        Ok(())
    }

    /// Get file contents from cache
    ///
    /// Returns None if file not in cache
    pub fn get(&self, path: &Path) -> Option<&String> {
        self.files.get(path)
    }

    /// Iterate over all files in cache
    ///
    /// Returns iterator over (path, content) pairs
    pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, &String)> {
        self.files.iter()
    }

    /// Get all .rs files in a specific directory from cache
    ///
    /// Returns iterator over (path, content) pairs
    pub fn get_rust_files_in_dir(&self, dir: &Path) -> Vec<(&PathBuf, &String)> {
        self.files
            .iter()
            .filter(|(path, _)| {
                path.starts_with(dir) && path.extension().is_some_and(|e| e == "rs")
            })
            .collect()
    }

    /// Get cache statistics
    ///
    /// Returns (file_count, total_bytes)
    pub fn stats(&self) -> (usize, usize) {
        let file_count = self.files.len();
        let total_bytes: usize = self.files.values().map(|s| s.len()).sum();
        (file_count, total_bytes)
    }

    /// Get cache age in milliseconds
    pub fn age_ms(&self) -> u128 {
        self.created_at.elapsed().as_millis()
    }
}

impl Default for FileCache {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// RustProjectScore - Main Score Container
// ============================================================================

/// Comprehensive Rust project quality score (v1.1)
///
/// Total score: 0-106 points across 6 categories
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RustProjectScore {
    /// Total score (0-106 points)
    pub total_score: f64,

    /// Letter grade (A+ to F)
    pub grade: Grade,

    /// Breakdown by category
    pub categories: CategoryScores,

    /// Actionable recommendations
    pub recommendations: Vec<Recommendation>,

    /// Metadata (timestamp, project, version)
    pub metadata: ScoreMetadata,

    /// Score velocity (Kaizen tracking) - NEW in v1.1
    pub velocity: Option<ScoreVelocity>,
}

impl RustProjectScore {
    /// Create a new score with zero values
    pub fn new() -> Self {
        Self {
            total_score: 0.0,
            grade: Grade::F,
            categories: CategoryScores::default(),
            recommendations: Vec::new(),
            metadata: ScoreMetadata::new("unknown".to_string(), "1.1.0".to_string()),
            velocity: None,
        }
    }
}

impl Default for RustProjectScore {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// Grade - Letter Grade Enum
// ============================================================================

/// Letter grade based on percentage of total possible points
///
/// Thresholds:
/// - A+ : 95-106 (89.6%+)
/// - A  : 90-94  (84.9%-89.5%)
/// - A- : 85-89  (80.2%-84.8%)
/// - B+ : 80-84  (75.5%-80.1%)
/// - B  : 70-79  (66.0%-75.4%)
/// - C  : 60-69
/// - D  : 50-59
/// - F  : 0-49
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Grade {
    APlus,
    A,
    AMinus,
    BPlus,
    B,
    C,
    D,
    F,
}

impl Grade {
    /// Calculate grade from score and max possible points
    pub fn from_score(score: f64, _max: f64) -> Self {
        if score >= 95.0 {
            Grade::APlus
        } else if score >= 90.0 {
            Grade::A
        } else if score >= 85.0 {
            Grade::AMinus
        } else if score >= 80.0 {
            Grade::BPlus
        } else if score >= 70.0 {
            Grade::B
        } else if score >= 60.0 {
            Grade::C
        } else if score >= 50.0 {
            Grade::D
        } else {
            Grade::F
        }
    }
}

impl fmt::Display for Grade {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Grade::APlus => write!(f, "A+"),
            Grade::A => write!(f, "A"),
            Grade::AMinus => write!(f, "A-"),
            Grade::BPlus => write!(f, "B+"),
            Grade::B => write!(f, "B"),
            Grade::C => write!(f, "C"),
            Grade::D => write!(f, "D"),
            Grade::F => write!(f, "F"),
        }
    }
}

// ============================================================================
// CategoryScores - 6 Scoring Categories
// ============================================================================

/// Six scoring categories (106 points total)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryScores {
    /// Rust tooling compliance (25pts)
    pub rust_tooling: CategoryScore,

    /// Code quality (26pts)
    pub code_quality: CategoryScore,

    /// Testing excellence (20pts)
    pub testing: CategoryScore,

    /// Documentation (15pts)
    pub documentation: CategoryScore,

    /// Performance & benchmarking (10pts)
    pub performance: CategoryScore,

    /// Dependency health (12pts)
    pub dependencies: CategoryScore,
}

impl CategoryScores {
    /// Calculate total score across all categories
    pub fn total(&self) -> f64 {
        self.rust_tooling.earned
            + self.code_quality.earned
            + self.testing.earned
            + self.documentation.earned
            + self.performance.earned
            + self.dependencies.earned
    }
}

impl Default for CategoryScores {
    fn default() -> Self {
        Self {
            rust_tooling: CategoryScore::new(0.0, 25.0),
            code_quality: CategoryScore::new(0.0, 26.0),
            testing: CategoryScore::new(0.0, 20.0),
            documentation: CategoryScore::new(0.0, 15.0),
            performance: CategoryScore::new(0.0, 10.0),
            dependencies: CategoryScore::new(0.0, 12.0),
        }
    }
}

// ============================================================================
// CategoryScore - Individual Category Metrics
// ============================================================================

/// Score for an individual category
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryScore {
    /// Points earned
    pub earned: f64,

    /// Maximum possible points
    pub max: f64,
}

impl CategoryScore {
    /// Create a new category score
    pub fn new(earned: f64, max: f64) -> Self {
        Self { earned, max }
    }

    /// Calculate percentage (0-100)
    pub fn percentage(&self) -> f64 {
        if self.max == 0.0 {
            0.0
        } else {
            (self.earned / self.max) * 100.0
        }
    }

    /// Check if category has perfect score
    pub fn is_perfect(&self) -> bool {
        (self.earned - self.max).abs() < 0.01
    }
}

// ============================================================================
// ScoreVelocity - Kaizen Continuous Improvement Tracking (NEW in v1.1)
// ============================================================================

/// Kaizen: Continuous improvement tracking
///
/// Tracks score changes over time to encourage incremental progress
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoreVelocity {
    /// Current score
    pub current: f64,

    /// Previous score (from baseline)
    pub previous: f64,

    /// Change in points
    pub delta: f64,

    /// Change as percentage
    pub delta_percent: f64,

    /// Days since baseline
    pub days_elapsed: u64,

    /// Points per day improvement rate
    pub points_per_day: f64,

    /// Most improved category
    pub most_improved: Option<String>,

    /// Projected days to next grade
    pub days_to_next_grade: Option<u64>,
}

impl ScoreVelocity {
    /// Calculate velocity from previous and current scores
    ///
    /// # Arguments
    /// * `previous` - Previous baseline score
    /// * `current` - Current score
    /// * `days` - Days elapsed since baseline
    pub fn calculate(previous: f64, current: f64, days: u64) -> Self {
        let delta = current - previous;
        let delta_percent = if previous == 0.0 {
            0.0
        } else {
            (delta / previous) * 100.0
        };

        let points_per_day = if days == 0 { 0.0 } else { delta / days as f64 };

        Self {
            current,
            previous,
            delta,
            delta_percent,
            days_elapsed: days,
            points_per_day,
            most_improved: None,
            days_to_next_grade: None,
        }
    }
}

// ============================================================================
// Recommendation - Actionable Improvement Suggestions
// ============================================================================

/// Priority level for recommendations
///
/// Ordered from highest to lowest priority for sorting
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum RecommendationPriority {
    Low,
    Medium,
    High,
    Critical,
}

/// Actionable recommendation for improvement
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recommendation {
    /// Category this recommendation applies to
    pub category: String,

    /// Description of the recommendation
    pub description: String,

    /// Priority level
    pub priority: RecommendationPriority,

    /// Potential points to gain
    pub potential_points: f64,
}

impl Recommendation {
    /// Create a new recommendation
    pub fn new(
        category: String,
        description: String,
        priority: RecommendationPriority,
        potential_points: f64,
    ) -> Self {
        Self {
            category,
            description,
            priority,
            potential_points,
        }
    }
}

// ============================================================================
// ScoreMetadata - Project Information
// ============================================================================

/// Metadata about the scoring analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoreMetadata {
    /// Timestamp of analysis
    pub timestamp: String,

    /// Project name
    pub project_name: String,

    /// Specification version
    pub version: String,
}

impl ScoreMetadata {
    /// Create new metadata
    pub fn new(project_name: String, version: String) -> Self {
        Self {
            timestamp: chrono::Utc::now().to_rfc3339(),
            project_name,
            version,
        }
    }
}
