//! Diff two capability manifests and flag dangerous transitions (a new
//! destructive tool, a tool whose class escalated, a newly exposed capability
//! tier). Complements schema drift-watch by tracking capability drift.

use std::collections::BTreeMap;

use serde::Serialize;

use super::model::{class_rank, CapabilityManifest, ToolCapability};

/// What changed about one tool between two manifests.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ChangeKind {
    /// A tool present in the new manifest but not the old.
    Added,
    /// A tool present in the old manifest but not the new.
    Removed,
    /// A tool whose side-effect class became more dangerous.
    ClassEscalated,
    /// A tool whose side-effect class became less dangerous.
    ClassRelaxed,
    /// A tool that gained a capability tier.
    TierAdded,
    /// A tool whose input schema fingerprint changed.
    SchemaChanged,
}

/// One change between two manifests.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CapabilityChange {
    /// Tool name.
    pub tool: String,
    /// What changed.
    pub kind: ChangeKind,
    /// Human-readable detail.
    pub detail: String,
    /// True when this change widens the attack surface.
    pub escalation: bool,
}

/// The result of diffing two manifests, in stable order.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct CapabilityDiff {
    /// Every change, sorted by tool then kind.
    pub changes: Vec<CapabilityChange>,
}

impl CapabilityDiff {
    /// True when any change widens the attack surface.
    pub fn has_escalation(&self) -> bool {
        self.changes.iter().any(|c| c.escalation)
    }
}

/// Diff `old` into `new`, producing the changes and flagging escalations.
pub fn diff_manifests(old: &CapabilityManifest, new: &CapabilityManifest) -> CapabilityDiff {
    let old_by: BTreeMap<&str, &ToolCapability> =
        old.tools.iter().map(|t| (t.name.as_str(), t)).collect();
    let new_by: BTreeMap<&str, &ToolCapability> =
        new.tools.iter().map(|t| (t.name.as_str(), t)).collect();

    let mut changes = Vec::new();

    for (name, n) in &new_by {
        match old_by.get(name) {
            None => {
                // A new mutating or destructive tool widens the surface.
                let escalation = class_rank(&n.class) >= 2 || !n.tiers.is_empty();
                changes.push(CapabilityChange {
                    tool: (*name).to_string(),
                    kind: ChangeKind::Added,
                    detail: format!("new {} tool", n.class),
                    escalation,
                });
            }
            Some(o) => changes.extend(diff_tool(name, o, n)),
        }
    }
    for name in old_by.keys() {
        if !new_by.contains_key(name) {
            changes.push(CapabilityChange {
                tool: (*name).to_string(),
                kind: ChangeKind::Removed,
                detail: "tool removed".to_string(),
                escalation: false,
            });
        }
    }

    changes.sort_by(|a, b| {
        a.tool
            .cmp(&b.tool)
            .then((a.kind as u8).cmp(&(b.kind as u8)))
    });
    CapabilityDiff { changes }
}

/// Diff one tool that exists in both manifests.
fn diff_tool(name: &str, old: &ToolCapability, new: &ToolCapability) -> Vec<CapabilityChange> {
    let mut out = Vec::new();
    let (old_rank, new_rank) = (class_rank(&old.class), class_rank(&new.class));
    if new_rank > old_rank {
        out.push(CapabilityChange {
            tool: name.to_string(),
            kind: ChangeKind::ClassEscalated,
            detail: format!("{} -> {}", old.class, new.class),
            escalation: true,
        });
    } else if new_rank < old_rank {
        out.push(CapabilityChange {
            tool: name.to_string(),
            kind: ChangeKind::ClassRelaxed,
            detail: format!("{} -> {}", old.class, new.class),
            escalation: false,
        });
    }
    let added: Vec<&String> = new
        .tiers
        .iter()
        .filter(|t| !old.tiers.contains(t))
        .collect();
    if !added.is_empty() {
        let names: Vec<&str> = added.iter().map(|s| s.as_str()).collect();
        out.push(CapabilityChange {
            tool: name.to_string(),
            kind: ChangeKind::TierAdded,
            detail: format!("gained tier(s): {}", names.join(", ")),
            escalation: true,
        });
    }
    if old.schema_fingerprint != new.schema_fingerprint {
        out.push(CapabilityChange {
            tool: name.to_string(),
            kind: ChangeKind::SchemaChanged,
            detail: format!("{} -> {}", old.schema_fingerprint, new.schema_fingerprint),
            escalation: false,
        });
    }
    out
}

/// Render a diff as a human-friendly summary.
pub fn render_diff(diff: &CapabilityDiff) -> String {
    if diff.changes.is_empty() {
        return "No capability changes.\n".to_string();
    }
    let mut s = String::from("Capability changes\n\n");
    for c in &diff.changes {
        let flag = if c.escalation { "  [escalation]" } else { "" };
        s.push_str(&format!(
            "  {:<16} {} :: {}{}\n",
            kind_label(c.kind),
            c.tool,
            c.detail,
            flag
        ));
    }
    let escalations = diff.changes.iter().filter(|c| c.escalation).count();
    s.push_str(&format!(
        "\n{} change(s), {} escalation(s)\n",
        diff.changes.len(),
        escalations
    ));
    s
}

/// Lowercase label for a change kind.
fn kind_label(kind: ChangeKind) -> &'static str {
    match kind {
        ChangeKind::Added => "added",
        ChangeKind::Removed => "removed",
        ChangeKind::ClassEscalated => "class-escalated",
        ChangeKind::ClassRelaxed => "class-relaxed",
        ChangeKind::TierAdded => "tier-added",
        ChangeKind::SchemaChanged => "schema-changed",
    }
}

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

    fn tool(name: &str, class: &str, tiers: &[&str], fp: &str) -> ToolCapability {
        ToolCapability {
            name: name.to_string(),
            class: class.to_string(),
            tiers: tiers.iter().map(|s| s.to_string()).collect(),
            annotated: false,
            schema_fingerprint: fp.to_string(),
        }
    }

    fn manifest(tools: Vec<ToolCapability>) -> CapabilityManifest {
        CapabilityManifest { tools }
    }

    #[test]
    fn identical_manifests_have_no_changes() {
        let m = manifest(vec![tool("a", "read-only", &[], "aa")]);
        let diff = diff_manifests(&m, &m);
        assert!(diff.changes.is_empty());
        assert!(!diff.has_escalation());
    }

    #[test]
    fn added_destructive_tool_is_an_escalation() {
        let old = manifest(vec![]);
        let new = manifest(vec![tool("wipe", "destructive", &["destructive"], "bb")]);
        let diff = diff_manifests(&old, &new);
        assert!(diff.has_escalation());
        assert_eq!(diff.changes[0].kind, ChangeKind::Added);
    }

    #[test]
    fn read_only_to_mutating_is_class_escalation() {
        let old = manifest(vec![tool("t", "read-only", &[], "aa")]);
        let new = manifest(vec![tool("t", "mutating", &[], "aa")]);
        let diff = diff_manifests(&old, &new);
        assert!(diff.has_escalation());
        assert!(diff
            .changes
            .iter()
            .any(|c| c.kind == ChangeKind::ClassEscalated));
    }

    #[test]
    fn relaxed_class_is_not_an_escalation() {
        let old = manifest(vec![tool("t", "destructive", &[], "aa")]);
        let new = manifest(vec![tool("t", "read-only", &[], "aa")]);
        let diff = diff_manifests(&old, &new);
        assert!(!diff.has_escalation());
        assert!(diff
            .changes
            .iter()
            .any(|c| c.kind == ChangeKind::ClassRelaxed));
    }

    #[test]
    fn removed_tool_and_schema_change_are_tracked_but_not_escalations() {
        let old = manifest(vec![
            tool("gone", "read-only", &[], "aa"),
            tool("t", "read-only", &[], "aa"),
        ]);
        let new = manifest(vec![tool("t", "read-only", &[], "zz")]);
        let diff = diff_manifests(&old, &new);
        assert!(diff.changes.iter().any(|c| c.kind == ChangeKind::Removed));
        assert!(diff
            .changes
            .iter()
            .any(|c| c.kind == ChangeKind::SchemaChanged));
        assert!(!diff.has_escalation());
    }
}
