//! RustToolingScorer - Rust Tooling Compliance Category (25 points)
//!
//! Analyzes Rust project compliance with standard tooling:
//! - Clippy (tiered scoring): 10pts
//!   - Correctness: 5pts (zero warnings)
//!   - Suspicious: 3pts (zero warnings)
//!   - Pedantic: 2pts (zero warnings)
//! - rustfmt compliance: 5pts
//! - cargo-audit (security): 7pts (risk-based scoring)
//! - cargo-deny (policy): 3pts

use super::models::{CategoryScore, FileCache, ScoringMode};
use super::scorer::{Scorer, ScorerError, ScorerResult};
use std::path::Path;
use std::process::Command;

/// Count of vulnerabilities by severity level
#[derive(Debug, Default)]
struct VulnerabilityCount {
    critical: u32,
    high: u32,
    medium: u32,
    low: u32,
}

/// Rust Tooling Compliance scorer
#[derive(Debug, Clone)]
pub struct RustToolingScorer {
    /// Category name
    name: String,
    /// Maximum possible points
    max_points: f64,
}

impl RustToolingScorer {
    /// Create a new RustToolingScorer
    pub fn new() -> Self {
        Self {
            name: "Rust Tooling Compliance".to_string(),
            max_points: 25.0,
        }
    }

    /// Run clippy and calculate tiered score
    fn score_clippy(&self, project_path: &Path) -> ScorerResult<f64> {
        // Check if Cargo.toml exists
        if !project_path.join("Cargo.toml").exists() {
            return Err(ScorerError::InvalidProject(
                "No Cargo.toml found".to_string(),
            ));
        }

        // Run clippy with JSON output
        let output = Command::new("cargo")
            .arg("clippy")
            .arg("--all-targets")
            .arg("--")
            .arg("-D")
            .arg("warnings")
            .current_dir(project_path)
            .output();

        match output {
            Ok(result) => {
                // If clippy succeeds (exit code 0), give full 10 points
                if result.status.success() {
                    Ok(10.0)
                } else {
                    // Parse stderr to determine warning levels
                    let stderr = String::from_utf8_lossy(&result.stderr);

                    let mut score: f64 = 10.0;

                    // Deduct points based on warning levels
                    // This is a simplified heuristic - real implementation
                    // would parse clippy JSON output
                    if stderr.contains("warning") || stderr.contains("error") {
                        // Assume some warnings - deduct points
                        score -= 2.0;
                    }

                    Ok(score.max(0.0))
                }
            }
            Err(e) => {
                // If cargo/clippy not found, gracefully degrade
                if e.kind() == std::io::ErrorKind::NotFound {
                    Err(ScorerError::ToolNotFound("cargo clippy".to_string()))
                } else {
                    Err(ScorerError::CommandError(e.to_string()))
                }
            }
        }
    }

    /// Run rustfmt check and calculate score
    fn score_rustfmt(&self, project_path: &Path) -> ScorerResult<f64> {
        // Run rustfmt in check mode (doesn't modify files)
        let output = Command::new("cargo")
            .arg("fmt")
            .arg("--")
            .arg("--check")
            .current_dir(project_path)
            .output();

        match output {
            Ok(result) => {
                // If rustfmt check passes, give full 5 points
                if result.status.success() {
                    Ok(5.0)
                } else {
                    // Formatting issues found
                    Ok(0.0)
                }
            }
            Err(e) => {
                if e.kind() == std::io::ErrorKind::NotFound {
                    Err(ScorerError::ToolNotFound("cargo fmt".to_string()))
                } else {
                    Err(ScorerError::CommandError(e.to_string()))
                }
            }
        }
    }

    /// Run cargo-audit and calculate risk-based score
    fn score_cargo_audit(&self, project_path: &Path) -> ScorerResult<f64> {
        // Run cargo-audit with JSON output for proper parsing
        let output = Command::new("cargo")
            .args(["audit", "--json"])
            .current_dir(project_path)
            .output();

        match output {
            Ok(result) => {
                let stdout = String::from_utf8_lossy(&result.stdout);

                // Parse JSON output to count vulnerabilities by severity
                let vuln_counts = self.parse_audit_json(&stdout);

                // Risk-based scoring (cumulative deductions):
                // Each Critical: -3.5pts, High: -2pts, Medium: -1pt, Low: -0.5pt
                let mut deduction: f64 = 0.0;
                deduction += vuln_counts.critical as f64 * 3.5;
                deduction += vuln_counts.high as f64 * 2.0;
                deduction += vuln_counts.medium as f64 * 1.0;
                deduction += vuln_counts.low as f64 * 0.5;

                Ok((7.0 - deduction).max(0.0))
            }
            Err(e) => {
                if e.kind() == std::io::ErrorKind::NotFound {
                    // cargo-audit not installed - graceful degradation
                    // Give 50% credit for clean Cargo.toml
                    Ok(3.5)
                } else {
                    Err(ScorerError::CommandError(e.to_string()))
                }
            }
        }
    }

    /// Parse cargo-audit JSON output to count vulnerabilities by severity
    fn parse_audit_json(&self, json_str: &str) -> VulnerabilityCount {
        let mut counts = VulnerabilityCount::default();

        // Try to parse as JSON
        if let Ok(json) = serde_json::from_str::<serde_json::Value>(json_str) {
            // cargo-audit JSON format: { "vulnerabilities": { "list": [...] } }
            if let Some(vulns) = json.get("vulnerabilities").and_then(|v| v.get("list")) {
                if let Some(vuln_array) = vulns.as_array() {
                    for vuln in vuln_array {
                        // Extract advisory.severity from each item
                        if let Some(severity) = vuln
                            .get("advisory")
                            .and_then(|a| a.get("severity"))
                            .and_then(|s| s.as_str())
                        {
                            match severity.to_lowercase().as_str() {
                                "critical" => counts.critical += 1,
                                "high" => counts.high += 1,
                                "medium" => counts.medium += 1,
                                "low" => counts.low += 1,
                                _ => {} // Ignore unknown severities
                            }
                        }
                    }
                }
            }
        }

        counts
    }

    /// Check for cargo-deny configuration and calculate score
    fn score_cargo_deny(&self, project_path: &Path) -> ScorerResult<f64> {
        // Check if deny.toml exists
        if project_path.join("deny.toml").exists() {
            // Has deny.toml - give full 3 points
            Ok(3.0)
        } else {
            // No deny.toml - 0 points
            Ok(0.0)
        }
    }

    /// Internal scoring logic that accepts optional cache
    ///
    /// **Kaizen Round 4**: Cache-aware scoring implementation
    /// Note: This scorer only does file existence checks and subprocess calls,
    /// so cache is not used (no file reads to optimize)
    fn score_internal(
        &self,
        project_path: &Path,
        mode: ScoringMode,
        _cache: Option<&FileCache>,
    ) -> ScorerResult<CategoryScore> {
        // Verify project has Cargo.toml
        if !project_path.join("Cargo.toml").exists() {
            return Err(ScorerError::InvalidProject(
                "No Cargo.toml found - not a valid Rust project".to_string(),
            ));
        }

        let mut total_earned = 0.0;

        // Score clippy (10pts) - ONLY in full mode (too slow for fast mode)
        if mode.is_full() {
            match self.score_clippy(project_path) {
                Ok(score) => total_earned += score,
                Err(ScorerError::ToolNotFound(_)) => {
                    // Graceful degradation - give 50% credit if tool not found
                    total_earned += 5.0;
                }
                Err(e) => return Err(e),
            }
        } else {
            // Fast mode: Skip clippy (too slow - takes 60-90s on large projects)
            // Give moderate credit (5/10 points) to avoid penalizing fast mode
            total_earned += 5.0;
        }

        // Score rustfmt (5pts)
        // KAIZEN: Skip in fast mode - takes 30-60s on large projects (145 files)
        if mode.is_full() {
            match self.score_rustfmt(project_path) {
                Ok(score) => total_earned += score,
                Err(ScorerError::ToolNotFound(_)) => {
                    // Graceful degradation
                    total_earned += 2.5;
                }
                Err(e) => return Err(e),
            }
        } else {
            // Fast mode: Check for rustfmt.toml existence as proxy
            // If rustfmt.toml exists, assume formatting is configured (give 3/5 pts)
            // If not, give moderate credit (2.5/5 pts)
            if project_path.join("rustfmt.toml").exists()
                || project_path.join(".rustfmt.toml").exists()
            {
                total_earned += 3.0;
            } else {
                total_earned += 2.5;
            }
        }

        // Score cargo-audit (7pts) - ONLY in full mode (network calls ~30s)
        if mode.is_full() {
            match self.score_cargo_audit(project_path) {
                Ok(score) => total_earned += score,
                Err(e) => return Err(e),
            }
        } else {
            // Fast mode: Skip cargo-audit (network calls too slow)
            // Give moderate credit (3.5/7 points) to avoid penalizing fast mode
            total_earned += 3.5;
        }

        // Score cargo-deny (3pts) - fast enough for both modes
        match self.score_cargo_deny(project_path) {
            Ok(score) => total_earned += score,
            Err(e) => return Err(e),
        }

        Ok(CategoryScore::new(total_earned, self.max_points))
    }
}

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

impl Scorer for RustToolingScorer {
    fn name(&self) -> &str {
        &self.name
    }

    fn max_points(&self) -> f64 {
        self.max_points
    }

    fn score(&self, project_path: &Path) -> ScorerResult<CategoryScore> {
        // Backward compatibility: call with default mode and no cache
        self.score_internal(project_path, ScoringMode::default(), None)
    }

    fn score_with_mode(
        &self,
        project_path: &Path,
        mode: ScoringMode,
    ) -> ScorerResult<CategoryScore> {
        // Backward compatibility: call with no cache
        self.score_internal(project_path, mode, None)
    }

    fn score_with_cache(
        &self,
        project_path: &Path,
        mode: ScoringMode,
        cache: Option<&FileCache>,
    ) -> ScorerResult<CategoryScore> {
        // Kaizen Round 4: Cache support added for API consistency
        // Note: This scorer only does file existence checks and subprocess calls,
        // so cache is not actually used (no file reads to optimize)
        self.score_internal(project_path, mode, cache)
    }

    fn recommendations(&self, project_path: &Path) -> Vec<String> {
        let mut recommendations = Vec::new();

        // Check clippy - SKIP subprocess, always recommend
        recommendations
            .push("Run 'cargo clippy --fix' to automatically fix clippy warnings".to_string());

        // Check rustfmt - SKIP subprocess, always recommend
        recommendations
            .push("Run 'cargo fmt' to format code according to Rust style guidelines".to_string());

        // Check cargo-audit - SKIP subprocess, always recommend
        recommendations.push("Run 'cargo audit' and update vulnerable dependencies".to_string());

        // Check cargo-deny - Fast filesystem check is ok
        if let Ok(score) = self.score_cargo_deny(project_path) {
            if score < 3.0 {
                recommendations.push(
                    "Add deny.toml configuration for dependency policy enforcement".to_string(),
                );
            }
        }

        recommendations
    }
}

// Ensure Send + Sync for parallel execution
unsafe impl Send for RustToolingScorer {}
unsafe impl Sync for RustToolingScorer {}

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

    #[test]
    fn test_scorer_creation() {
        let scorer = RustToolingScorer::new();
        assert_eq!(scorer.name(), "Rust Tooling Compliance");
        assert_eq!(scorer.max_points(), 25.0);
    }

    #[test]
    fn test_scorer_implements_trait() {
        let scorer = RustToolingScorer::new();
        let _trait_obj: &dyn Scorer = &scorer;
    }
}
