//! Handler for `mcptest manifest`: emit and diff a capability SBOM.
//!
//! Emit classifies a server's tools (from a `tools/list` snapshot or a live
//! `--url`) by side-effect into a stable YAML/JSON manifest. Diff compares two
//! saved manifests and flags capability drift, gating CI with
//! `--fail-on-escalation`.

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

use mcptest_core::capabilities::{
    diff_manifests, manifest_from_tools, render_diff, CapabilityManifest,
};

use crate::cli::args::{ManifestArgs, ManifestCommand, ManifestDiffArgs, ManifestFormat};

/// Run `mcptest manifest` (emit) or its `diff` subcommand.
pub(crate) fn manifest_command(args: ManifestArgs) -> Result<i32> {
    if let Some(ManifestCommand::Diff(diff_args)) = args.command {
        return diff_command(diff_args);
    }

    let tools = load_tools(&args)?;
    let manifest = manifest_from_tools(&tools);
    match args.format {
        ManifestFormat::Json => println!("{}", serde_json::to_string_pretty(&manifest)?),
        // Pretty has no distinct manifest shape; emit YAML.
        _ => print!("{}", serde_yaml::to_string(&manifest)?),
    }
    Ok(0)
}

/// Diff two saved manifests.
fn diff_command(args: ManifestDiffArgs) -> Result<i32> {
    let old = load_manifest(&args.old)?;
    let new = load_manifest(&args.new)?;
    let diff = diff_manifests(&old, &new);
    match args.format {
        ManifestFormat::Json => println!("{}", serde_json::to_string_pretty(&diff)?),
        ManifestFormat::Yaml => print!("{}", serde_yaml::to_string(&diff)?),
        ManifestFormat::Pretty => print!("{}", render_diff(&diff)),
    }
    if args.fail_on_escalation && diff.has_escalation() {
        eprintln!("manifest diff: a capability escalation was detected");
        return Ok(1);
    }
    Ok(0)
}

/// Load the tool descriptors to classify, from `--url` or a snapshot file.
fn load_tools(args: &ManifestArgs) -> Result<Vec<Value>> {
    if let Some(url) = &args.url {
        let token = args
            .bearer_token_env
            .as_ref()
            .and_then(|var| std::env::var(var).ok());
        let runtime =
            tokio::runtime::Runtime::new().context("could not build the tokio runtime")?;
        let surface = runtime
            .block_on(crate::commands::discover::probe_server(url, token))
            .map_err(|e| anyhow::anyhow!("probing {url}: {e}"))?;
        return Ok(surface.tools);
    }
    let Some(path) = &args.snapshot else {
        anyhow::bail!("provide a tools/list snapshot path or --url");
    };
    let body =
        std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
    let value: Value =
        serde_json::from_str(&body).with_context(|| format!("parsing {}", path.display()))?;
    Ok(value
        .get("tools")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default())
}

/// Load a saved manifest, accepting YAML or JSON (YAML is a JSON superset).
fn load_manifest(path: &std::path::Path) -> Result<CapabilityManifest> {
    let body =
        std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
    serde_yaml::from_str(&body).with_context(|| format!("parsing {} as a manifest", path.display()))
}
