//! Claim ceilings and reportability metadata.
//!
//! Cortex must distinguish local development mechanics from signed,
//! externally anchored, or authority-grade claims. This module provides the
//! small lattice used by higher layers to downgrade mixed evidence to the
//! weakest truthful claim.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::proof::ProofState;

/// Runtime mode that bounds what Cortex may truthfully claim.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeMode {
    /// Runtime mode was not supplied or could not be verified.
    Unknown,
    /// Development-only execution. No durable authority claim should escape.
    Dev,
    /// Remote API call (e.g. Anthropic, external hosted model) whose response
    /// is unsigned and whose provenance cannot be locally verified. Trust
    /// ceiling is lower than `LocalUnsigned` because the execution boundary
    /// is outside the operator's machine. See ADR 0037 and ADR 0048 prep.
    RemoteUnsigned,
    /// Local unsigned append or local-only metadata.
    LocalUnsigned,
    /// Signed local ledger evidence is available.
    SignedLocalLedger,
    /// Signed evidence is bound to an external anchor surface.
    ExternallyAnchored,
    /// Authority-grade mode: signed, anchored, policy-gated, and suitable for
    /// high-authority reporting.
    AuthorityGrade,
}

impl RuntimeMode {
    /// Highest claim ceiling this runtime mode can support.
    #[must_use]
    pub const fn claim_ceiling(self) -> ClaimCeiling {
        match self {
            Self::Unknown => ClaimCeiling::DevOnly,
            Self::Dev => ClaimCeiling::DevOnly,
            Self::RemoteUnsigned => ClaimCeiling::LocalUnsigned,
            Self::LocalUnsigned => ClaimCeiling::LocalUnsigned,
            Self::SignedLocalLedger => ClaimCeiling::SignedLocalLedger,
            Self::ExternallyAnchored => ClaimCeiling::ExternallyAnchored,
            Self::AuthorityGrade => ClaimCeiling::AuthorityGrade,
        }
    }
}

/// Authority class of the evidence supporting a claim.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityClass {
    /// Source may be observed but must not influence durable authority.
    Untrusted,
    /// Derived artifact such as a summary or reflection candidate.
    Derived,
    /// Audit-visible observed source with local influence only.
    Observed,
    /// Operator-approved source for bounded automation.
    Verified,
    /// Human root of trust or policy-equivalent authority for this deployment.
    Operator,
}

/// Proof state as it participates in claim reporting.
///
/// `Unknown` is explicit because ADR 0037 forbids silently treating absent
/// proof closure as local unsigned authority.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ClaimProofState {
    /// No proof closure was supplied.
    Unknown,
    /// The proof closure is broken and can support diagnostics only.
    Broken,
    /// The proof closure is incomplete but no broken edge was observed.
    Partial,
    /// Every required proof edge for the claim passed.
    FullChainVerified,
}

impl ClaimProofState {
    /// Highest claim ceiling this proof state can support.
    #[must_use]
    pub const fn claim_ceiling(self) -> ClaimCeiling {
        match self {
            Self::Unknown | Self::Broken => ClaimCeiling::DevOnly,
            Self::Partial => ClaimCeiling::LocalUnsigned,
            Self::FullChainVerified => ClaimCeiling::AuthorityGrade,
        }
    }
}

impl From<ProofState> for ClaimProofState {
    fn from(value: ProofState) -> Self {
        match value {
            ProofState::FullChainVerified => Self::FullChainVerified,
            ProofState::Partial => Self::Partial,
            ProofState::Broken => Self::Broken,
        }
    }
}

impl AuthorityClass {
    /// Highest claim ceiling this authority class can support.
    #[must_use]
    pub const fn claim_ceiling(self) -> ClaimCeiling {
        match self {
            Self::Untrusted => ClaimCeiling::DevOnly,
            Self::Derived | Self::Observed => ClaimCeiling::LocalUnsigned,
            Self::Verified => ClaimCeiling::SignedLocalLedger,
            Self::Operator => ClaimCeiling::AuthorityGrade,
        }
    }

    /// Weakest authority class from an iterator.
    #[must_use]
    pub fn weakest<I>(classes: I) -> Option<Self>
    where
        I: IntoIterator<Item = Self>,
    {
        classes.into_iter().min()
    }
}

/// Maximum truthful reporting level for a claim.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ClaimCeiling {
    /// Development-only; not reportable as durable evidence.
    DevOnly,
    /// Local unsigned mechanics only.
    LocalUnsigned,
    /// Signed local ledger claim.
    SignedLocalLedger,
    /// Externally anchored ledger claim.
    ExternallyAnchored,
    /// Authority-grade claim suitable for high-authority reporting.
    AuthorityGrade,
}

impl ClaimCeiling {
    /// Weakest ceiling from an iterator.
    #[must_use]
    pub fn weakest<I>(ceilings: I) -> Option<Self>
    where
        I: IntoIterator<Item = Self>,
    {
        ceilings.into_iter().min()
    }

    /// Weaken this ceiling to the weaker of two ceilings.
    #[must_use]
    pub fn mix_to_weakest(self, other: Self) -> Self {
        self.min(other)
    }
}

/// Metadata for a claim that may be shown, exported, or used as authority.
///
/// `effective_ceiling` is private and always recomputed from runtime mode,
/// authority class, and requested ceiling. Callers can ask for a high ceiling,
/// but they cannot construct a reportable claim above its weakest evidence.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ReportableClaim {
    claim: String,
    runtime_mode: RuntimeMode,
    authority_class: AuthorityClass,
    proof_state: ClaimProofState,
    requested_ceiling: ClaimCeiling,
    effective_ceiling: ClaimCeiling,
    downgrade_reasons: Vec<String>,
}

impl ReportableClaim {
    /// Construct claim metadata with the effective ceiling clamped to the
    /// weakest supporting signal.
    #[must_use]
    pub fn new(
        claim: impl Into<String>,
        runtime_mode: RuntimeMode,
        authority_class: AuthorityClass,
        proof_state: ClaimProofState,
        requested_ceiling: ClaimCeiling,
    ) -> Self {
        let mut downgrade_reasons = Vec::new();
        let effective_ceiling = effective_ceiling(
            runtime_mode,
            authority_class,
            proof_state,
            requested_ceiling,
        );

        if effective_ceiling < requested_ceiling {
            downgrade_reasons.push(format!(
                "requested ceiling {requested_ceiling:?} downgraded to {effective_ceiling:?}"
            ));
        }
        if proof_state != ClaimProofState::FullChainVerified {
            downgrade_reasons.push(format!(
                "proof state {proof_state:?} limits authority claims"
            ));
        }

        Self {
            claim: claim.into(),
            runtime_mode,
            authority_class,
            proof_state,
            requested_ceiling,
            effective_ceiling,
            downgrade_reasons,
        }
    }

    /// Claim text.
    #[must_use]
    pub fn claim(&self) -> &str {
        &self.claim
    }

    /// Runtime mode used to evaluate this claim.
    #[must_use]
    pub const fn runtime_mode(&self) -> RuntimeMode {
        self.runtime_mode
    }

    /// Authority class used to evaluate this claim.
    #[must_use]
    pub const fn authority_class(&self) -> AuthorityClass {
        self.authority_class
    }

    /// Proof state used to evaluate this claim.
    #[must_use]
    pub const fn proof_state(&self) -> ClaimProofState {
        self.proof_state
    }

    /// Ceiling requested by the producer.
    #[must_use]
    pub const fn requested_ceiling(&self) -> ClaimCeiling {
        self.requested_ceiling
    }

    /// Effective ceiling after weakest-link downgrade.
    #[must_use]
    pub const fn effective_ceiling(&self) -> ClaimCeiling {
        self.effective_ceiling
    }

    /// Reasons this claim was downgraded.
    #[must_use]
    pub fn downgrade_reasons(&self) -> &[String] {
        &self.downgrade_reasons
    }

    /// Return a copy downgraded to the supplied ceiling.
    #[must_use]
    pub fn downgraded_to(mut self, ceiling: ClaimCeiling, reason: impl Into<String>) -> Self {
        self.requested_ceiling = self.requested_ceiling.min(ceiling);
        self.effective_ceiling = effective_ceiling(
            self.runtime_mode,
            self.authority_class,
            self.proof_state,
            self.requested_ceiling,
        );
        self.downgrade_reasons.push(reason.into());
        self
    }

    /// Mix this claim with another claim and return the weakest truthful
    /// metadata for the combined assertion.
    #[must_use]
    pub fn mix_to_weakest(mut self, other: &Self, claim: impl Into<String>) -> Self {
        self.claim = claim.into();
        self.runtime_mode = self.runtime_mode.min(other.runtime_mode);
        self.authority_class = self.authority_class.min(other.authority_class);
        self.proof_state = self.proof_state.min(other.proof_state);
        self.requested_ceiling = self.requested_ceiling.min(other.requested_ceiling);
        self.effective_ceiling = self.effective_ceiling.min(other.effective_ceiling);
        self.downgrade_reasons
            .extend(other.downgrade_reasons.iter().cloned());
        self.downgrade_reasons
            .push("mixed claim downgraded to weakest contributing claim".into());
        self
    }
}

/// Compute the effective ceiling for the supplied signals.
#[must_use]
pub fn effective_ceiling(
    runtime_mode: RuntimeMode,
    authority_class: AuthorityClass,
    proof_state: ClaimProofState,
    requested_ceiling: ClaimCeiling,
) -> ClaimCeiling {
    ClaimCeiling::weakest([
        runtime_mode.claim_ceiling(),
        authority_class.claim_ceiling(),
        proof_state.claim_ceiling(),
        requested_ceiling,
    ])
    .expect("fixed-size ceiling set is non-empty")
}

/// Compute the weakest authority class for mixed evidence.
#[must_use]
pub fn mix_authority_to_weakest<I>(classes: I) -> Option<AuthorityClass>
where
    I: IntoIterator<Item = AuthorityClass>,
{
    AuthorityClass::weakest(classes)
}

/// Compute the weakest claim ceiling for mixed evidence.
#[must_use]
pub fn mix_claims_to_weakest<I>(ceilings: I) -> Option<ClaimCeiling>
where
    I: IntoIterator<Item = ClaimCeiling>,
{
    ClaimCeiling::weakest(ceilings)
}

/// Compute the weakest effective ceiling across reportable claims.
#[must_use]
pub fn mix_reportable_claims_to_weakest<'a, I>(claims: I) -> Option<ClaimCeiling>
where
    I: IntoIterator<Item = &'a ReportableClaim>,
{
    claims
        .into_iter()
        .map(ReportableClaim::effective_ceiling)
        .min()
}

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

    #[test]
    fn runtime_mode_caps_claim_ceiling() {
        assert_eq!(RuntimeMode::Unknown.claim_ceiling(), ClaimCeiling::DevOnly);
        assert_eq!(RuntimeMode::Dev.claim_ceiling(), ClaimCeiling::DevOnly);
        assert_eq!(
            RuntimeMode::RemoteUnsigned.claim_ceiling(),
            ClaimCeiling::LocalUnsigned
        );
        assert_eq!(
            RuntimeMode::LocalUnsigned.claim_ceiling(),
            ClaimCeiling::LocalUnsigned
        );
        assert_eq!(
            RuntimeMode::SignedLocalLedger.claim_ceiling(),
            ClaimCeiling::SignedLocalLedger
        );
        assert_eq!(
            RuntimeMode::ExternallyAnchored.claim_ceiling(),
            ClaimCeiling::ExternallyAnchored
        );
        assert_eq!(
            RuntimeMode::AuthorityGrade.claim_ceiling(),
            ClaimCeiling::AuthorityGrade
        );
    }

    #[test]
    fn authority_class_caps_claim_ceiling() {
        assert_eq!(
            AuthorityClass::Untrusted.claim_ceiling(),
            ClaimCeiling::DevOnly
        );
        assert_eq!(
            AuthorityClass::Derived.claim_ceiling(),
            ClaimCeiling::LocalUnsigned
        );
        assert_eq!(
            AuthorityClass::Observed.claim_ceiling(),
            ClaimCeiling::LocalUnsigned
        );
        assert_eq!(
            AuthorityClass::Verified.claim_ceiling(),
            ClaimCeiling::SignedLocalLedger
        );
        assert_eq!(
            AuthorityClass::Operator.claim_ceiling(),
            ClaimCeiling::AuthorityGrade
        );
    }

    #[test]
    fn reportable_claim_clamps_to_weakest_signal() {
        let claim = ReportableClaim::new(
            "phase 2 mechanics verified",
            RuntimeMode::LocalUnsigned,
            AuthorityClass::Operator,
            ClaimProofState::FullChainVerified,
            ClaimCeiling::AuthorityGrade,
        );

        assert_eq!(claim.effective_ceiling(), ClaimCeiling::LocalUnsigned);
        assert!(!claim.downgrade_reasons().is_empty());
    }

    #[test]
    fn verified_source_cannot_lift_dev_runtime() {
        let claim = ReportableClaim::new(
            "trusted run history",
            RuntimeMode::Dev,
            AuthorityClass::Verified,
            ClaimProofState::FullChainVerified,
            ClaimCeiling::SignedLocalLedger,
        );

        assert_eq!(claim.effective_ceiling(), ClaimCeiling::DevOnly);
    }

    #[test]
    fn proof_state_caps_claim_ceiling() {
        let partial = ReportableClaim::new(
            "operator action observed",
            RuntimeMode::AuthorityGrade,
            AuthorityClass::Operator,
            ClaimProofState::Partial,
            ClaimCeiling::AuthorityGrade,
        );
        let broken = ReportableClaim::new(
            "trusted run history",
            RuntimeMode::AuthorityGrade,
            AuthorityClass::Operator,
            ClaimProofState::Broken,
            ClaimCeiling::AuthorityGrade,
        );
        let unknown = ReportableClaim::new(
            "export-ready evidence",
            RuntimeMode::AuthorityGrade,
            AuthorityClass::Operator,
            ClaimProofState::Unknown,
            ClaimCeiling::AuthorityGrade,
        );

        assert_eq!(partial.effective_ceiling(), ClaimCeiling::LocalUnsigned);
        assert_eq!(broken.effective_ceiling(), ClaimCeiling::DevOnly);
        assert_eq!(unknown.effective_ceiling(), ClaimCeiling::DevOnly);
    }

    #[test]
    fn mixed_claims_use_weakest_effective_ceiling() {
        let strong = ReportableClaim::new(
            "anchored ledger tip",
            RuntimeMode::ExternallyAnchored,
            AuthorityClass::Operator,
            ClaimProofState::FullChainVerified,
            ClaimCeiling::ExternallyAnchored,
        );
        let weak = ReportableClaim::new(
            "development ledger append",
            RuntimeMode::LocalUnsigned,
            AuthorityClass::Observed,
            ClaimProofState::Partial,
            ClaimCeiling::AuthorityGrade,
        );

        assert_eq!(
            mix_reportable_claims_to_weakest([&strong, &weak]),
            Some(ClaimCeiling::LocalUnsigned)
        );

        let mixed = strong.mix_to_weakest(&weak, "combined claim");
        assert_eq!(mixed.effective_ceiling(), ClaimCeiling::LocalUnsigned);
    }

    #[test]
    fn runtime_mode_wire_strings_are_stable() {
        assert_eq!(
            serde_json::to_value(RuntimeMode::Unknown).unwrap(),
            serde_json::json!("unknown")
        );
        assert_eq!(
            serde_json::to_value(RuntimeMode::Dev).unwrap(),
            serde_json::json!("dev")
        );
        assert_eq!(
            serde_json::to_value(RuntimeMode::RemoteUnsigned).unwrap(),
            serde_json::json!("remote_unsigned")
        );
        assert_eq!(
            serde_json::to_value(RuntimeMode::LocalUnsigned).unwrap(),
            serde_json::json!("local_unsigned")
        );
        assert_eq!(
            serde_json::to_value(RuntimeMode::SignedLocalLedger).unwrap(),
            serde_json::json!("signed_local_ledger")
        );
        assert_eq!(
            serde_json::to_value(RuntimeMode::ExternallyAnchored).unwrap(),
            serde_json::json!("externally_anchored")
        );
        assert_eq!(
            serde_json::to_value(RuntimeMode::AuthorityGrade).unwrap(),
            serde_json::json!("authority_grade")
        );
    }
}
