//! Handler for `mcptest security proof` (WOR-1430).
//!
//! Walk a source tree, scan each recognized file, and emit an advisory report
//! of tool-handler-to-sink leads. SARIF carries source locations; the PoC suite
//! (gated behind `--authorize` and `--target`) is the path to dynamic
//! confirmation against a disposable target.

use std::path::Path;

use anyhow::{Context, Result};

use mcptest_core::security::source_proof::{
    build_report, scan_source, to_poc_suite, to_sarif, Language, ProofReport,
};
use mcptest_core::security::SecuritySeverity;

use crate::cli::args::{SecurityFailOn, SecurityProofArgs, SecurityProofFormat};

/// Run the source-proof scan and render the report.
pub(crate) fn proof_command(args: SecurityProofArgs) -> Result<i32> {
    let report = scan_tree(&args.src)?;

    match args.format {
        SecurityProofFormat::Json => println!("{}", serde_json::to_string_pretty(&report)?),
        SecurityProofFormat::Sarif => {
            println!("{}", serde_json::to_string_pretty(&to_sarif(&report))?)
        }
        SecurityProofFormat::Pretty => print_pretty(&report),
    }

    if let Some(poc_path) = &args.poc {
        // The arg parser already requires --authorize and --target alongside
        // --poc; this is the defensive second check before writing exploit-shaped
        // scaffolding.
        let target = args.target.as_deref().unwrap_or("./your-disposable-server");
        let suite = to_poc_suite(&report, target);
        std::fs::write(poc_path, suite)
            .with_context(|| format!("writing PoC suite to {}", poc_path.display()))?;
        eprintln!(
            "wrote a reproducible PoC suite to {} (advisory; run only against a disposable target)",
            poc_path.display()
        );
    }

    if let Some(fail_on) = args.fail_on {
        if report.fails_at(severity_of(fail_on)) {
            eprintln!(
                "source-proof: an advisory finding is at or above {:?}",
                fail_on
            );
            return Ok(1);
        }
    }
    Ok(0)
}

/// Scan a file or directory tree into one report.
fn scan_tree(root: &Path) -> Result<ProofReport> {
    let mut all_findings = Vec::new();
    let mut all_handlers = Vec::new();
    for path in source_files(root)? {
        let Some(language) = path
            .extension()
            .and_then(|e| e.to_str())
            .and_then(Language::from_extension)
        else {
            continue;
        };
        let text = std::fs::read_to_string(&path)
            .with_context(|| format!("reading {}", path.display()))?;
        let (findings, handlers) = scan_source(&path.display().to_string(), language, &text);
        all_findings.extend(findings);
        all_handlers.extend(handlers);
    }
    Ok(build_report(
        &root.display().to_string(),
        all_findings,
        all_handlers,
    ))
}

/// Collect recognized source files under a path (a single file, or a tree).
fn source_files(root: &Path) -> Result<Vec<std::path::PathBuf>> {
    if root.is_file() {
        return Ok(vec![root.to_path_buf()]);
    }
    let mut out = Vec::new();
    collect_dir(root, &mut out)?;
    out.sort();
    Ok(out)
}

/// Recurse a directory, skipping vendored trees, collecting source files.
fn collect_dir(dir: &Path, out: &mut Vec<std::path::PathBuf>) -> Result<()> {
    for entry in std::fs::read_dir(dir).with_context(|| format!("reading dir {}", dir.display()))? {
        let path = entry?.path();
        let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
        if path.is_dir() {
            if matches!(name, "node_modules" | "target" | ".git" | "dist" | "build") {
                continue;
            }
            collect_dir(&path, out)?;
        } else if path
            .extension()
            .and_then(|e| e.to_str())
            .and_then(Language::from_extension)
            .is_some()
        {
            out.push(path);
        }
    }
    Ok(())
}

/// 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,
    }
}

/// Render the advisory report as a human-friendly table.
fn print_pretty(report: &ProofReport) {
    println!("Source-aware vulnerability proofing (advisory)");
    println!();
    println!(
        "  scanned {}; resolved {} tool handler(s)",
        report.root,
        report.handlers.len()
    );
    println!();
    if report.findings.is_empty() {
        println!("  no risky sinks mapped to a tool handler");
        return;
    }
    for f in &report.findings {
        let flow = if f.tainted {
            "tainted flow"
        } else {
            "proximity lead"
        };
        println!(
            "  {:<8} {:<22} {} ({}:{}) [{}, {} confidence]",
            f.severity.as_str(),
            f.category.label(),
            f.tool.as_deref().unwrap_or("<unmapped>"),
            f.file,
            f.line,
            flow,
            f.confidence,
        );
        let label = if f.tainted { "flow" } else { "lead" };
        println!("           {label}: {}", f.snippet);
        println!("           fix:  {}", f.category.remediation());
    }
    println!();
    println!(
        "  {} advisory finding(s). Static leads, not proofs; confirm with the PoC suite \
         against a disposable target.",
        report.findings.len()
    );
}
