//! Core authority, trust, and capability primitives for McPact.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use thiserror::Error;

/// Authority class for a generated MCP tool.
///
/// Canonical 6-variant ladder, ordered from least to most invasive:
/// `Observe < Analyze < Plan < Mutate < Destroy < Credential`. The wire
/// format is PascalCase (`"Analyze"`, etc.); these strings are the
/// load-bearing form referenced by generated manifests, `authority.schema.json`,
/// and downstream consumers (`corcept-mcp`, `aegress-mcp`, `taudit-mcp`).
///
/// # Vernacular note (`Analyze` vs `Derive`)
///
/// Some early operator-side documentation referred to the second tier as
/// `"Derive"`. There is no `Derive` variant — `Analyze` is the canonical
/// spelling and the only one accepted by `serde`. Manifests that say
/// `"Derive"` will fail to deserialize. This alignment is anchored in
/// the 2026-05-20 portability audit (see
/// `ecosystem-catalog/standardisation/gates/portability-audit-2026-05-20.md`
/// §4) and the G-2 schema-cohesion quality gate.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "PascalCase")]
pub enum AuthorityClass {
    /// Reads state and returns it without material transformation.
    Observe,
    /// Reads state and derives analysis or findings.
    ///
    /// Operator vernacular: sometimes spoken as "Derive". Canonical
    /// spelling on the wire is `"Analyze"`.
    Analyze,
    /// Produces a proposed mutation without applying it.
    Plan,
    /// Changes local or remote state.
    Mutate,
    /// Deletes, revokes, destroys, or irreversibly removes state.
    Destroy,
    /// Reads, writes, mints, exchanges, or scopes credentials/secrets.
    Credential,
}

impl AuthorityClass {
    /// The canonical 6-variant ladder, ordered from least to most invasive.
    ///
    /// Stable contract: any change to this slice is a schema-cohesion
    /// (G-2) break and requires a coordinated downstream regen of every
    /// generated MCP adapter. Anchored by
    /// [`tests::authority_class_canonical_variants_snapshot`] which
    /// fails-closed if the set ever drifts.
    pub const CANONICAL_VARIANTS: &'static [Self] = &[
        Self::Observe,
        Self::Analyze,
        Self::Plan,
        Self::Mutate,
        Self::Destroy,
        Self::Credential,
    ];

    /// The canonical wire-format names of [`Self::CANONICAL_VARIANTS`].
    ///
    /// Used by `mcpact-generate::render_authority_schema` to keep the
    /// emitted `authority.schema.json` in lockstep with this enum. If a
    /// new variant is added here, the schema snapshot test in
    /// `mcpact-generate` fails-closed until the schema is updated.
    pub const CANONICAL_NAMES: &'static [&'static str] = &[
        "Observe",
        "Analyze",
        "Plan",
        "Mutate",
        "Destroy",
        "Credential",
    ];

    /// Returns true when this authority can change or expose sensitive state.
    #[must_use]
    pub const fn is_high_risk(self) -> bool {
        matches!(self, Self::Mutate | Self::Destroy | Self::Credential)
    }

    /// Default approval posture for the class.
    #[must_use]
    pub const fn default_approval(self) -> ApprovalMode {
        if self.is_high_risk() {
            ApprovalMode::Always
        } else {
            ApprovalMode::Never
        }
    }
}

impl Display for AuthorityClass {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            Self::Observe => "Observe",
            Self::Analyze => "Analyze",
            Self::Plan => "Plan",
            Self::Mutate => "Mutate",
            Self::Destroy => "Destroy",
            Self::Credential => "Credential",
        };
        f.write_str(s)
    }
}

/// Trust ceiling for a tool spec. A ceiling limits what the runtime may assume.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "PascalCase")]
pub enum TrustCeiling {
    /// Generated by machine inspection; never safe by default.
    Inferred,
    /// Human-reviewed local manifest.
    Reviewed,
    /// Signed by a trusted publisher or workspace key.
    Signed,
    /// Signed and verified against fixtures, policy, and conformance checks.
    Verified,
}

impl Display for TrustCeiling {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            Self::Inferred => "Inferred",
            Self::Reviewed => "Reviewed",
            Self::Signed => "Signed",
            Self::Verified => "Verified",
        };
        f.write_str(s)
    }
}

/// Human approval requirement for a tool invocation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum ApprovalMode {
    /// No runtime approval is needed.
    Never,
    /// Approval is required when policy classifies the action as mutating.
    OnMutation,
    /// Approval is required for every invocation.
    Always,
}

/// Declared side-effect domain.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum EffectKind {
    /// Read from local filesystem.
    FilesystemRead,
    /// Write to local filesystem.
    FilesystemWrite,
    /// Read over network.
    NetworkRead,
    /// Write over network.
    NetworkWrite,
    /// Spawn a child process.
    ProcessSpawn,
    /// Read secrets or credentials.
    SecretRead,
    /// Write secrets or credentials.
    SecretWrite,
    /// Read from cloud APIs.
    CloudRead,
    /// Write to cloud APIs.
    CloudWrite,
    /// Unclassified side effect.
    Unknown,
}

/// Structured policy decision.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PolicyDecision {
    /// Whether execution is allowed.
    pub allowed: bool,
    /// Human-readable reason.
    pub reason: String,
}

impl PolicyDecision {
    /// Allow with a reason.
    #[must_use]
    pub fn allow(reason: impl Into<String>) -> Self {
        Self {
            allowed: true,
            reason: reason.into(),
        }
    }

    /// Deny with a reason.
    #[must_use]
    pub fn deny(reason: impl Into<String>) -> Self {
        Self {
            allowed: false,
            reason: reason.into(),
        }
    }
}

/// Common McPact error type for core boundary failures.
#[derive(Debug, Error)]
pub enum McPactError {
    /// Invalid manifest, policy, or tool contract.
    #[error("invalid contract: {0}")]
    InvalidContract(String),
    /// Policy denied execution.
    #[error("policy denied execution: {0}")]
    PolicyDenied(String),
    /// Execution failed.
    #[error("execution failed: {0}")]
    Execution(String),
    /// Serialization or deserialization failed.
    #[error("serialization failed: {0}")]
    Serde(String),
}

/// Result alias.
pub type Result<T> = std::result::Result<T, McPactError>;

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

    #[test]
    fn authority_high_risk_defaults_to_approval() {
        assert_eq!(
            AuthorityClass::Mutate.default_approval(),
            ApprovalMode::Always
        );
        assert_eq!(
            AuthorityClass::Destroy.default_approval(),
            ApprovalMode::Always
        );
        assert_eq!(
            AuthorityClass::Credential.default_approval(),
            ApprovalMode::Always
        );
        assert_eq!(
            AuthorityClass::Observe.default_approval(),
            ApprovalMode::Never
        );
    }

    #[test]
    fn serde_uses_product_names() {
        let s = serde_json::to_string(&AuthorityClass::Analyze).unwrap();
        assert_eq!(s, "\"Analyze\"");
    }

    /// G-2 schema-cohesion gate (anchor: portability-audit-2026-05-20 §4).
    ///
    /// Locks the canonical `AuthorityClass` enum to its 6-variant set in
    /// PascalCase wire form. If a new variant is added, this test must be
    /// updated in the *same* commit as the schema, the generator, and the
    /// downstream regen of `corcept-mcp`, `aegress-mcp`, `taudit-mcp`.
    ///
    /// In particular: there is **no** `Derive` variant. `Analyze` is the
    /// canonical spelling. Operator-vernacular `"Derive"` does NOT
    /// deserialize and must NOT appear in any manifest or doctrine.
    #[test]
    fn authority_class_canonical_variants_snapshot() {
        let names: Vec<String> = AuthorityClass::CANONICAL_VARIANTS
            .iter()
            .map(|variant| {
                serde_json::to_string(variant)
                    .unwrap()
                    .trim_matches('"')
                    .to_string()
            })
            .collect();
        assert_eq!(
            names,
            vec![
                "Observe".to_string(),
                "Analyze".to_string(),
                "Plan".to_string(),
                "Mutate".to_string(),
                "Destroy".to_string(),
                "Credential".to_string(),
            ],
            "AuthorityClass canonical variants drifted; coordinate downstream regen \
             before changing"
        );
        // CANONICAL_NAMES must mirror the wire form one-for-one.
        let canonical_names: Vec<String> = AuthorityClass::CANONICAL_NAMES
            .iter()
            .map(|s| (*s).to_string())
            .collect();
        assert_eq!(
            canonical_names, names,
            "AuthorityClass::CANONICAL_NAMES out of sync with serialized form"
        );
        // No `Derive` variant — guard against future accidental rename.
        for name in &names {
            assert_ne!(
                name, "Derive",
                "the canonical name is `Analyze`, not `Derive` \
                 (see mcpact-core::AuthorityClass docs)"
            );
        }
    }

    #[test]
    fn trust_ceiling_orders_by_assurance() {
        assert!(TrustCeiling::Inferred < TrustCeiling::Reviewed);
        assert!(TrustCeiling::Reviewed < TrustCeiling::Signed);
        assert!(TrustCeiling::Signed < TrustCeiling::Verified);
    }

    #[test]
    fn policy_decision_helpers() {
        let allow = PolicyDecision::allow("ok");
        let deny = PolicyDecision::deny("no");
        assert!(allow.allowed);
        assert!(!deny.allowed);
    }
}
