//! Handler for `mcptest policy <subcommand>`.
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};

use anyhow::{Context, Result};
use mcptest_core::policy::{
    evaluate, facts_from_certification, facts_from_conformance, facts_from_evidence,
    facts_from_model_compat, facts_from_run_report, facts_from_security, Facts, PolicyFile,
    PolicyOutcome, RuleStatus, Verdict,
};

use crate::cli::args::{PolicyArgs, PolicyCommand, PolicyFormat, PolicySimulateArgs};

/// Dispatch `mcptest policy` to its one subcommand.
///
/// Kept as a thin router so adding a second `policy` verb later is a one-line
/// match arm, matching the shape of the other grouped subcommands.
pub(crate) fn policy_command(args: PolicyArgs) -> Result<i32> {
    match args.command {
        PolicyCommand::Simulate(a) => simulate_command(a),
    }
}

/// Run `mcptest policy simulate`.
///
/// Loads the declarative policy, builds a fact map from whichever artifacts the
/// caller passed, evaluates the policy at the current instant, and prints the
/// verdict. The exit code is the governance signal: a dry-run always exits 0 so
/// the verdict is informational, while `--gate` exits 1 on a failing verdict so
/// CI can block on it.
fn simulate_command(args: PolicySimulateArgs) -> Result<i32> {
    let body = std::fs::read_to_string(&args.policy)
        .with_context(|| format!("failed to read policy {}", args.policy.display()))?;
    let policy: PolicyFile = serde_yaml::from_str(&body)
        .with_context(|| format!("failed to parse policy {}", args.policy.display()))?;

    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs() as i64)
        .unwrap_or(0);

    let facts = collect_facts(&args, now)?;
    let outcome = evaluate(&policy, &facts, now);

    match args.format {
        PolicyFormat::Json => println!("{}", serde_json::to_string_pretty(&outcome)?),
        PolicyFormat::Pretty => print_pretty(&outcome),
    }

    if args.gate && outcome.failed() {
        Ok(1)
    } else {
        Ok(0)
    }
}

/// Read and extend the fact map from every artifact path the caller supplied.
///
/// Pulled out of [`simulate_command`] so that function stays short and the set
/// of artifact-to-extractor wirings reads as one table.
fn collect_facts(args: &PolicySimulateArgs, now: i64) -> Result<Facts> {
    let mut facts = Facts::new();
    if let Some(path) = &args.run_report {
        facts.extend(facts_from_run_report(&read_json(path)?));
    }
    if let Some(path) = &args.judge_cert {
        facts.extend(facts_from_certification(&read_json(path)?, now));
    }
    if let Some(path) = &args.conformance_report {
        facts.extend(facts_from_conformance(&read_json(path)?));
    }
    if let Some(path) = &args.security {
        facts.extend(facts_from_security(&read_json(path)?));
    }
    if let Some(path) = &args.model_compat {
        facts.extend(facts_from_model_compat(&read_json(path)?));
    }
    if let Some(path) = &args.evidence {
        facts.extend(facts_from_evidence(&read_json(path)?));
    }
    Ok(facts)
}

/// Print the verdict and one cited line per rule in a human-readable block.
fn print_pretty(outcome: &PolicyOutcome) {
    let verdict = match outcome.verdict {
        Verdict::Pass => "pass",
        Verdict::Warn => "warn",
        Verdict::Fail => "fail",
    };
    println!("policy: {verdict}");
    for rule in &outcome.rules {
        println!(
            "  [{}] {} ({}): {}",
            tag(rule.status),
            rule.id,
            rule.fact,
            rule.detail
        );
    }
}

/// Map a rule status to its fixed-width display tag.
fn tag(status: RuleStatus) -> &'static str {
    match status {
        RuleStatus::Pass => "PASS",
        RuleStatus::Fail => "FAIL",
        RuleStatus::Warn => "WARN",
        RuleStatus::Waived => "WAIVED",
        RuleStatus::ExpiredWaiver => "EXPIRED-WAIVER",
        RuleStatus::Unevaluated => "UNEVALUATED",
    }
}

/// Read a JSON artifact from disk into a [`serde_json::Value`], attaching the
/// path to any read or parse error so a missing or malformed input is obvious.
fn read_json(path: &Path) -> Result<serde_json::Value> {
    let body = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read artifact {}", path.display()))?;
    serde_json::from_str(&body)
        .with_context(|| format!("failed to parse artifact {}", path.display()))
}

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

    #[test]
    fn read_json_reports_the_path_on_a_missing_file() {
        let err = read_json(Path::new("/nope/does-not-exist.json")).unwrap_err();
        assert!(err.to_string().contains("does-not-exist.json"));
    }
}
