//! Capability manifest model: a stable, diffable inventory of what a server's
//! tools can do, classified by side-effect. The manifest is the behavioral
//! companion to a dependency SBOM: it tracks what an agent can *do*, not what it
//! is built from, so a reviewer can diff capability drift across runs.

use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};

use crate::exec_policy::{classify_descriptor, ToolClass};
use crate::security::toxic_flow::classify_tool;

/// One tool's capability record.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolCapability {
    /// Tool name.
    pub name: String,
    /// Side-effect class: `read-only`, `read-only-presumed`, `mutating`, or
    /// `destructive`.
    pub class: String,
    /// Capability tiers the tool exposes (for example `sensitive-data-exposure`,
    /// `destructive`), sorted for stable output.
    pub tiers: Vec<String>,
    /// Whether the tool declares MCP behavior annotations.
    pub annotated: bool,
    /// Short fingerprint of the input schema, so a shape change shows in a diff.
    pub schema_fingerprint: String,
}

/// A server's capability manifest: every tool classified, in stable name order.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityManifest {
    /// Tools, sorted by name.
    pub tools: Vec<ToolCapability>,
}

/// Stable string for a [`ToolClass`].
pub fn class_str(class: ToolClass) -> &'static str {
    match class {
        ToolClass::ReadOnly => "read-only",
        ToolClass::ReadOnlyPresumed => "read-only-presumed",
        ToolClass::Mutating => "mutating",
        ToolClass::Destructive => "destructive",
    }
}

/// Escalation rank of a class (higher is more dangerous). Read-only and
/// read-only-presumed share a rank: the presumption is about confidence, not
/// capability.
pub fn class_rank(class: &str) -> u8 {
    match class {
        "destructive" => 3,
        "mutating" => 2,
        "read-only" | "read-only-presumed" => 1,
        _ => 0,
    }
}

/// Build a manifest from a list of raw `tools/list` tool descriptors.
pub fn manifest_from_tools(tools: &[Value]) -> CapabilityManifest {
    let mut out: Vec<ToolCapability> = tools
        .iter()
        .map(|t| {
            let name = t
                .get("name")
                .and_then(Value::as_str)
                .unwrap_or("<unnamed>")
                .to_string();
            let mut tiers: Vec<String> = classify_tool(t)
                .iter()
                .map(|c| c.as_str().to_string())
                .collect();
            tiers.sort();
            tiers.dedup();
            ToolCapability {
                name,
                class: class_str(classify_descriptor(t)).to_string(),
                tiers,
                annotated: t.get("annotations").is_some_and(|a| !a.is_null()),
                schema_fingerprint: fingerprint(t.get("inputSchema")),
            }
        })
        .collect();
    out.sort_by(|a, b| a.name.cmp(&b.name));
    CapabilityManifest { tools: out }
}

/// A 12-hex-char SHA-256 of the input schema, or `none` when absent. serde_json
/// serializes object keys in sorted order by default, so the digest is stable
/// for an unchanged schema.
fn fingerprint(schema: Option<&Value>) -> String {
    let Some(v) = schema else {
        return "none".to_string();
    };
    // to_string on a Value is infallible in practice; fall back to empty so a
    // pathological value still yields a deterministic digest rather than panic.
    let canon = serde_json::to_string(v).unwrap_or_default();
    let digest = Sha256::digest(canon.as_bytes());
    digest.iter().take(6).map(|b| format!("{b:02x}")).collect()
}

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

    fn tools() -> Vec<Value> {
        vec![
            json!({"name": "read_file", "annotations": {"readOnlyHint": true},
                   "inputSchema": {"type": "object"}}),
            json!({"name": "delete_record", "inputSchema": {"type": "object"}}),
        ]
    }

    #[test]
    fn manifest_classifies_and_sorts_by_name() {
        let m = manifest_from_tools(&tools());
        assert_eq!(m.tools.len(), 2);
        // Sorted: delete_record before read_file.
        assert_eq!(m.tools[0].name, "delete_record");
        assert_eq!(m.tools[0].class, "destructive");
        assert_eq!(m.tools[1].name, "read_file");
        assert_eq!(m.tools[1].class, "read-only");
        assert!(m.tools[1].annotated);
        assert!(!m.tools[0].annotated);
    }

    #[test]
    fn manifest_is_deterministic() {
        assert_eq!(manifest_from_tools(&tools()), manifest_from_tools(&tools()));
    }

    #[test]
    fn class_rank_orders_escalation() {
        assert!(class_rank("destructive") > class_rank("mutating"));
        assert!(class_rank("mutating") > class_rank("read-only"));
        assert_eq!(class_rank("read-only"), class_rank("read-only-presumed"));
    }

    #[test]
    fn fingerprint_is_stable_and_absent_is_none() {
        let a = manifest_from_tools(&tools());
        let b = manifest_from_tools(&tools());
        assert_eq!(a.tools[1].schema_fingerprint, b.tools[1].schema_fingerprint);
        let no_schema = manifest_from_tools(&[json!({"name": "x"})]);
        assert_eq!(no_schema.tools[0].schema_fingerprint, "none");
    }
}
