//! Proof closure state for ledger, lineage, and authority verification.
//!
//! This module is intentionally pure shape logic for `cortex-core`: no I/O,
//! no hashing, no signature verification, and no ledger reads. Verifier crates
//! supply the observed edges and failures; these types preserve the result
//! without allowing a partial proof to be accidentally reported as full.

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

use crate::policy::{compose_policy_outcomes, PolicyContribution, PolicyDecision, PolicyOutcome};
use crate::{CoreError, CoreResult};

/// Overall state of a proof closure check.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProofState {
    /// Every required edge was present and verified.
    FullChainVerified,
    /// Some required edge is missing or unresolved, but no contradiction was
    /// observed.
    Partial,
    /// At least one required edge was observed and found invalid.
    Broken,
}

/// Kind of edge participating in a proof closure.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProofEdgeKind {
    /// Event-to-event hash-chain continuity.
    HashChain,
    /// Payload or envelope signature verification.
    Signature,
    /// Identity rotation continuity.
    IdentityRotation,
    /// External anchor binding for a ledger tip.
    ExternalAnchor,
    /// Summary, memory, or context lineage closure.
    LineageClosure,
    /// Authority fold or effective-trust propagation.
    AuthorityFold,
    /// Context pack linkage back to selected source artifacts.
    ContextPackLink,
}

/// A verified proof edge.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ProofEdge {
    /// Edge category.
    pub kind: ProofEdgeKind,
    /// Stable source reference for the edge.
    pub from_ref: String,
    /// Stable target reference for the edge.
    pub to_ref: String,
    /// Optional evidence reference such as a signature id, anchor id, or audit
    /// row id.
    pub evidence_ref: Option<String>,
}

impl ProofEdge {
    /// Construct a verified proof edge.
    #[must_use]
    pub fn new(
        kind: ProofEdgeKind,
        from_ref: impl Into<String>,
        to_ref: impl Into<String>,
    ) -> Self {
        Self {
            kind,
            from_ref: from_ref.into(),
            to_ref: to_ref.into(),
            evidence_ref: None,
        }
    }

    /// Attach an evidence reference.
    #[must_use]
    pub fn with_evidence_ref(mut self, evidence_ref: impl Into<String>) -> Self {
        self.evidence_ref = Some(evidence_ref.into());
        self
    }
}

/// Failure category for a proof edge.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProofEdgeFailure {
    /// The expected edge was not found.
    Missing,
    /// The verifier could not resolve enough data to decide.
    Unresolved,
    /// The edge exists but does not match the expected hash or reference.
    Mismatch,
    /// The signature edge failed cryptographic verification.
    InvalidSignature,
    /// The external anchor does not bind the expected ledger tip.
    AnchorMismatch,
    /// Authority or effective-trust recomputation disagreed with the cached
    /// value.
    AuthorityMismatch,
}

impl ProofEdgeFailure {
    /// Whether this failure proves the chain is broken rather than merely
    /// incomplete.
    #[must_use]
    pub const fn is_broken(self) -> bool {
        match self {
            Self::Missing | Self::Unresolved => false,
            Self::Mismatch
            | Self::InvalidSignature
            | Self::AnchorMismatch
            | Self::AuthorityMismatch => true,
        }
    }
}

/// A missing, unresolved, or invalid proof edge.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct FailingEdge {
    /// Edge category.
    pub kind: ProofEdgeKind,
    /// Stable source reference for the edge.
    pub from_ref: String,
    /// Optional target reference when known.
    pub to_ref: Option<String>,
    /// Failure category.
    pub failure: ProofEdgeFailure,
    /// Operator-facing explanation. This should name the failed invariant, not
    /// contain secret material.
    pub reason: String,
}

impl FailingEdge {
    /// Construct a missing-edge failure.
    #[must_use]
    pub fn missing(
        kind: ProofEdgeKind,
        from_ref: impl Into<String>,
        reason: impl Into<String>,
    ) -> Self {
        Self {
            kind,
            from_ref: from_ref.into(),
            to_ref: None,
            failure: ProofEdgeFailure::Missing,
            reason: reason.into(),
        }
    }

    /// Construct an unresolved-edge failure.
    #[must_use]
    pub fn unresolved(
        kind: ProofEdgeKind,
        from_ref: impl Into<String>,
        reason: impl Into<String>,
    ) -> Self {
        Self {
            kind,
            from_ref: from_ref.into(),
            to_ref: None,
            failure: ProofEdgeFailure::Unresolved,
            reason: reason.into(),
        }
    }

    /// Construct a broken-edge failure.
    #[must_use]
    pub fn broken(
        kind: ProofEdgeKind,
        from_ref: impl Into<String>,
        to_ref: impl Into<String>,
        failure: ProofEdgeFailure,
        reason: impl Into<String>,
    ) -> Self {
        debug_assert!(failure.is_broken());
        let failure = if failure.is_broken() {
            failure
        } else {
            ProofEdgeFailure::Mismatch
        };

        Self {
            kind,
            from_ref: from_ref.into(),
            to_ref: Some(to_ref.into()),
            failure,
            reason: reason.into(),
        }
    }

    /// Whether this failed edge proves a broken proof closure.
    #[must_use]
    pub const fn is_broken(&self) -> bool {
        self.failure.is_broken()
    }
}

/// Closure report for a proof graph.
///
/// The `state` field is private by design. Callers either create a full report
/// with [`ProofClosureReport::full_chain_verified`] or let
/// [`ProofClosureReport::from_edges`] compute the weakest truthful state from
/// the supplied failures.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ProofClosureReport {
    #[serde(rename = "proof_state")]
    state: ProofState,
    verified_edges: Vec<ProofEdge>,
    failing_edges: Vec<FailingEdge>,
}

impl ProofClosureReport {
    /// Construct a report that has no failing edges.
    #[must_use]
    pub fn full_chain_verified(verified_edges: Vec<ProofEdge>) -> Self {
        Self {
            state: ProofState::FullChainVerified,
            verified_edges,
            failing_edges: Vec::new(),
        }
    }

    /// Construct a report and compute state from the supplied failing edges.
    ///
    /// Any broken failure makes the report [`ProofState::Broken`]. Any
    /// non-empty missing or unresolved failure list makes it
    /// [`ProofState::Partial`]. Only an empty failure list can be full.
    #[must_use]
    pub fn from_edges(verified_edges: Vec<ProofEdge>, failing_edges: Vec<FailingEdge>) -> Self {
        let state = classify_failures(&failing_edges);
        Self {
            state,
            verified_edges,
            failing_edges,
        }
    }

    /// Current proof state.
    #[must_use]
    pub const fn state(&self) -> ProofState {
        self.state
    }

    /// Verified edges.
    #[must_use]
    pub fn verified_edges(&self) -> &[ProofEdge] {
        &self.verified_edges
    }

    /// Missing, unresolved, or invalid edges.
    #[must_use]
    pub fn failing_edges(&self) -> &[FailingEdge] {
        &self.failing_edges
    }

    /// Whether the report is fully verified.
    #[must_use]
    pub const fn is_full_chain_verified(&self) -> bool {
        matches!(self.state, ProofState::FullChainVerified)
    }

    /// Whether the report has at least one broken edge.
    #[must_use]
    pub const fn is_broken(&self) -> bool {
        matches!(self.state, ProofState::Broken)
    }

    /// Add a failing edge and recompute the weakest truthful state.
    pub fn push_failing_edge(&mut self, edge: FailingEdge) {
        self.failing_edges.push(edge);
        self.state = classify_failures(&self.failing_edges);
    }

    /// Add a failing edge and return the updated report.
    #[must_use]
    pub fn with_failing_edge(mut self, edge: FailingEdge) -> Self {
        self.push_failing_edge(edge);
        self
    }

    /// Derive the ADR 0026 policy decision for this proof closure.
    #[must_use]
    pub fn policy_decision(&self) -> PolicyDecision {
        let outcome = match self.state {
            ProofState::FullChainVerified => PolicyOutcome::Allow,
            ProofState::Partial => PolicyOutcome::Quarantine,
            ProofState::Broken => PolicyOutcome::Reject,
        };
        let reason = match self.state {
            ProofState::FullChainVerified => "proof closure is fully verified",
            ProofState::Partial => {
                "proof closure is partial and cannot be treated as clean authority"
            }
            ProofState::Broken => "proof closure is broken and fails closed",
        };
        compose_policy_outcomes(
            vec![
                PolicyContribution::new("proof_closure.state", outcome, reason)
                    .expect("static policy contribution is valid"),
            ],
            None,
        )
    }

    /// Fail closed before this proof report is consumed as current authority.
    pub fn require_current_use_allowed(&self) -> CoreResult<()> {
        let policy = self.policy_decision();
        match policy.final_outcome {
            PolicyOutcome::Reject | PolicyOutcome::Quarantine => {
                Err(CoreError::Validation(format!(
                    "proof closure current use blocked by policy outcome {:?}",
                    policy.final_outcome
                )))
            }
            PolicyOutcome::Allow | PolicyOutcome::Warn | PolicyOutcome::BreakGlass => Ok(()),
        }
    }
}

const fn classify_failures(failing_edges: &[FailingEdge]) -> ProofState {
    if failing_edges.is_empty() {
        return ProofState::FullChainVerified;
    }

    let mut i = 0;
    while i < failing_edges.len() {
        if failing_edges[i].is_broken() {
            return ProofState::Broken;
        }
        i += 1;
    }

    ProofState::Partial
}

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

    fn edge() -> ProofEdge {
        ProofEdge::new(ProofEdgeKind::HashChain, "evt_a", "evt_b").with_evidence_ref("hash_ab")
    }

    #[test]
    fn full_report_cannot_carry_failures() {
        let report = ProofClosureReport::full_chain_verified(vec![edge()]);
        assert_eq!(report.state(), ProofState::FullChainVerified);
        assert!(report.is_full_chain_verified());
        assert!(report.failing_edges().is_empty());
    }

    #[test]
    fn missing_edge_downgrades_to_partial() {
        let report = ProofClosureReport::from_edges(
            vec![edge()],
            vec![FailingEdge::missing(
                ProofEdgeKind::ExternalAnchor,
                "tip_1",
                "anchor not available",
            )],
        );

        assert_eq!(report.state(), ProofState::Partial);
        assert!(!report.is_full_chain_verified());
        assert!(!report.is_broken());
    }

    #[test]
    fn broken_edge_downgrades_to_broken() {
        let report = ProofClosureReport::from_edges(
            vec![edge()],
            vec![FailingEdge::broken(
                ProofEdgeKind::Signature,
                "evt_a",
                "sig_a",
                ProofEdgeFailure::InvalidSignature,
                "signature verification failed",
            )],
        );

        assert_eq!(report.state(), ProofState::Broken);
        assert!(report.is_broken());
    }

    #[test]
    fn adding_failure_recomputes_state() {
        let mut report = ProofClosureReport::full_chain_verified(vec![edge()]);
        report.push_failing_edge(FailingEdge::unresolved(
            ProofEdgeKind::LineageClosure,
            "mem_a",
            "source event not loaded",
        ));

        assert_eq!(report.state(), ProofState::Partial);
        assert!(!report.is_full_chain_verified());
    }

    #[test]
    fn proof_state_wire_strings_are_stable() {
        assert_eq!(
            serde_json::to_value(ProofState::FullChainVerified).unwrap(),
            serde_json::json!("full_chain_verified")
        );
        assert_eq!(
            serde_json::to_value(ProofState::Partial).unwrap(),
            serde_json::json!("partial")
        );
        assert_eq!(
            serde_json::to_value(ProofState::Broken).unwrap(),
            serde_json::json!("broken")
        );
    }

    #[test]
    fn proof_report_serializes_proof_state_field() {
        let report = ProofClosureReport::from_edges(
            Vec::new(),
            vec![FailingEdge::missing(
                ProofEdgeKind::ExternalAnchor,
                "tip_1",
                "anchor not available",
            )],
        );
        let serialized = serde_json::to_value(report).unwrap();

        assert_eq!(serialized["proof_state"], serde_json::json!("partial"));
        assert!(serialized.get("state").is_none());
    }

    #[test]
    fn proof_state_derives_policy_decision() {
        let full = ProofClosureReport::full_chain_verified(Vec::new());
        let partial = ProofClosureReport::from_edges(
            Vec::new(),
            vec![FailingEdge::missing(
                ProofEdgeKind::LineageClosure,
                "memory:mem_01",
                "source missing",
            )],
        );
        let broken = ProofClosureReport::from_edges(
            Vec::new(),
            vec![FailingEdge::broken(
                ProofEdgeKind::HashChain,
                "event:a",
                "event:b",
                ProofEdgeFailure::Mismatch,
                "hash mismatch",
            )],
        );

        assert_eq!(full.policy_decision().final_outcome, PolicyOutcome::Allow);
        assert_eq!(
            partial.policy_decision().final_outcome,
            PolicyOutcome::Quarantine
        );
        assert_eq!(
            broken.policy_decision().final_outcome,
            PolicyOutcome::Reject
        );
    }

    #[test]
    fn partial_or_broken_proof_fails_closed_for_current_use() {
        let partial = ProofClosureReport::from_edges(
            Vec::new(),
            vec![FailingEdge::missing(
                ProofEdgeKind::LineageClosure,
                "memory:mem_01",
                "source missing",
            )],
        );
        let broken = ProofClosureReport::from_edges(
            Vec::new(),
            vec![FailingEdge::broken(
                ProofEdgeKind::HashChain,
                "event:a",
                "event:b",
                ProofEdgeFailure::Mismatch,
                "hash mismatch",
            )],
        );

        assert!(partial.require_current_use_allowed().is_err());
        assert!(broken.require_current_use_allowed().is_err());
        ProofClosureReport::full_chain_verified(Vec::new())
            .require_current_use_allowed()
            .expect("full chain proof supports current use");
    }
}
