//! Handler for `mcptest scan`: a static security scan of MCP config files.
//!
//! Reads a config file (or discovers local client configs), runs the
//! [`mcptest_core::config_scan`] rules, and renders findings plus a per-server
//! risk score. With `--fail-on` it returns a non-zero exit so CI can gate.

use anyhow::{Context, Result};
use serde_json::Value;

use mcptest_core::config_scan::{
    parse_config, render_pretty, scan_entries, to_html, to_sarif, ConfigFinding, ConfigScanReport,
};
use mcptest_core::security::SecuritySeverity;
use mcptest_core::waivers::{partition, render_summary, CivilDate, WaiverFile};

use crate::cli::args::{ScanArgs, ScanFormat, SecurityFailOn};

/// Run the config scan and render the report.
pub(crate) fn scan_command(args: ScanArgs) -> Result<i32> {
    let report = match &args.path {
        Some(path) => {
            let body = std::fs::read_to_string(path)
                .with_context(|| format!("reading {}", path.display()))?;
            let entries = parse_config(&body)
                .with_context(|| format!("parsing {} as an MCP config", path.display()))?;
            scan_entries(&path.display().to_string(), &entries)
        }
        None => scan_discovered()?,
    };

    match args.format {
        ScanFormat::Pretty => print!("{}", render_pretty(&report)),
        ScanFormat::Json => println!("{}", serde_json::to_string_pretty(&report)?),
        ScanFormat::Sarif => println!("{}", serde_json::to_string_pretty(&to_sarif(&report))?),
        ScanFormat::Html => print!("{}", to_html(&report)),
    }

    // Waivers affect only gating: the report above still shows every finding.
    let active: Option<Vec<ConfigFinding>> = match &args.waivers {
        Some(path) => {
            let body = std::fs::read_to_string(path)
                .with_context(|| format!("reading waiver file {}", path.display()))?;
            let file =
                WaiverFile::parse(&body).with_context(|| format!("parsing {}", path.display()))?;
            let (active, waived) =
                partition(report.findings.clone(), &file, CivilDate::today_utc());
            let summary = render_summary(&waived);
            if !summary.is_empty() {
                eprint!("{summary}");
            }
            Some(active)
        }
        None => None,
    };

    if let Some(fail_on) = args.fail_on {
        let threshold = severity_of(fail_on);
        let fails = match &active {
            Some(findings) => findings.iter().any(|f| f.severity >= threshold),
            None => report.fails_at(threshold),
        };
        if fails {
            eprintln!(
                "config scan: a finding is at or above {}",
                threshold.as_str()
            );
            return Ok(1);
        }
    }
    Ok(0)
}

/// Build a report from servers discovered in local client configs.
///
/// Discovery yields a typed launch spec, so client-specific keys such as
/// `autoApprove` are not visible here; CFG-004 only fires when scanning a file
/// directly. The secret, broad-filesystem, and missing-credential checks all
/// still apply.
fn scan_discovered() -> Result<ConfigScanReport> {
    use mcptest_core::discovery::clients::discover_all_clients;
    let mut entries: Vec<(String, Value)> = Vec::new();
    for server in discover_all_clients() {
        let value = serde_json::to_value(&server.server_config)
            .context("serializing a discovered server config")?;
        entries.push((server.name, value));
    }
    Ok(scan_entries("discovered clients", &entries))
}

/// Map the CLI fail-on enum onto the core severity.
fn severity_of(fail_on: SecurityFailOn) -> SecuritySeverity {
    match fail_on {
        SecurityFailOn::Info => SecuritySeverity::Info,
        SecurityFailOn::Low => SecuritySeverity::Low,
        SecurityFailOn::Medium => SecuritySeverity::Medium,
        SecurityFailOn::High => SecuritySeverity::High,
        SecurityFailOn::Critical => SecuritySeverity::Critical,
    }
}
