//! AXIOM constraint export for context packs.
//!
//! Cortex can supply context to AXIOM agents, but the pack must also carry the
//! limits AXIOM has to obey: truth ceiling, unknown proof state, conflicts,
//! redaction boundaries, and the absence of execution authority.

use cortex_core::{
    AllowedClaimLanguage, AxiomConstraint, AxiomConstraintKind, AxiomConstraintSeverity,
    BoundaryContradictionState, BoundaryQuarantineState, BoundaryRedactionState, ClaimCeiling,
    ClaimProofState, ContextPackId, CortexAxiomConstraintEnvelopeV1, FailingEdge, PolicyOutcome,
    ProofClosureReport, ProofEdgeFailure, ProofEdgeKind, ProvenanceClass,
};
use serde::{Deserialize, Serialize};

use crate::pack::{ContextPack, ContextRefId};
use crate::redaction::{ContentRedaction, PackMode, RawEventPayloadPolicy};

/// Constraint export from one Cortex context pack to AXIOM.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AxiomContextExport {
    /// Context pack id the constraints apply to.
    pub context_pack_id: ContextPackId,
    /// Task the pack was built for.
    pub task: String,
    /// Weakest effective claim ceiling across selected refs and pack state.
    pub claim_ceiling: ClaimCeiling,
    /// ADR 0026 policy outcome for the built pack.
    pub policy_outcome: PolicyOutcome,
    /// Selected refs whose proof state is unknown.
    pub unknown_refs: Vec<ContextRefId>,
    /// Selected refs whose proof state is partial or broken.
    pub limited_proof_refs: Vec<ContextRefId>,
    /// Conflict ids surfaced in the context pack.
    pub conflict_refs: Vec<String>,
    /// Redaction limits AXIOM must not infer around.
    pub redaction_limits: Vec<String>,
    /// Constraints AXIOM must obey.
    pub constraints: Vec<AxiomConstraint>,
}

/// Export AXIOM constraints from a context pack.
#[must_use]
pub fn axiom_export_for_pack(pack: &ContextPack) -> AxiomContextExport {
    let unknown_refs = pack
        .selected_refs
        .iter()
        .filter(|selected| selected.proof_state == ClaimProofState::Unknown)
        .map(|selected| selected.ref_id.clone())
        .collect::<Vec<_>>();
    let limited_proof_refs = pack
        .selected_refs
        .iter()
        .filter(|selected| selected.proof_state != ClaimProofState::FullChainVerified)
        .map(|selected| selected.ref_id.clone())
        .collect::<Vec<_>>();
    let conflict_refs = pack
        .conflicts
        .iter()
        .map(|conflict| conflict.contradiction_id.to_string())
        .collect::<Vec<_>>();
    let redaction_limits = redaction_limits(pack);
    let claim_ceiling = pack_claim_ceiling(pack);
    let policy = pack.policy_decision();
    let mut constraints = Vec::new();

    constraints.push(AxiomConstraint::new(
        AxiomConstraintKind::NoExecutionAuthority,
        AxiomConstraintSeverity::Hard,
        "context pack constraints do not grant execution authority or tool permission",
    ));
    constraints.push(AxiomConstraint::new(
        AxiomConstraintKind::TruthCeiling,
        AxiomConstraintSeverity::Limit,
        format!("AXIOM output must not claim above {claim_ceiling:?} for this pack"),
    ));

    if !unknown_refs.is_empty() {
        constraints.push(
            AxiomConstraint::new(
                AxiomConstraintKind::ProofStateLimit,
                AxiomConstraintSeverity::Limit,
                "one or more selected refs have unknown proof state",
            )
            .with_refs(refs_to_strings(&unknown_refs)),
        );
    }

    if !limited_proof_refs.is_empty() {
        constraints.push(
            AxiomConstraint::new(
                AxiomConstraintKind::ForbidPromotionShapedOutput,
                AxiomConstraintSeverity::Hard,
                "limited proof state forbids promotion-shaped output",
            )
            .with_refs(refs_to_strings(&limited_proof_refs)),
        );
    }

    if !conflict_refs.is_empty() {
        constraints.push(
            AxiomConstraint::new(
                AxiomConstraintKind::ConflictPresent,
                AxiomConstraintSeverity::Hard,
                "context pack contains unresolved or surfaced conflicts",
            )
            .with_refs(conflict_refs.clone()),
        );
        constraints.push(AxiomConstraint::new(
            AxiomConstraintKind::ForbidPromotionShapedOutput,
            AxiomConstraintSeverity::Hard,
            "conflicting context forbids promotion-shaped output",
        ));
    }

    if claim_ceiling < ClaimCeiling::SignedLocalLedger {
        constraints.push(AxiomConstraint::new(
            AxiomConstraintKind::LowTrust,
            AxiomConstraintSeverity::Hard,
            "low trust or unsigned context forbids durable authority claims",
        ));
        constraints.push(AxiomConstraint::new(
            AxiomConstraintKind::ForbidPromotionShapedOutput,
            AxiomConstraintSeverity::Hard,
            "low-trust context forbids promotion-shaped output",
        ));
    }

    if !redaction_limits.is_empty() {
        constraints.push(AxiomConstraint::new(
            AxiomConstraintKind::RedactionBoundary,
            AxiomConstraintSeverity::Hard,
            "redaction policy forbids inferring or reconstructing omitted raw context",
        ));
    }

    AxiomContextExport {
        context_pack_id: pack.context_pack_id,
        task: pack.task.clone(),
        claim_ceiling,
        policy_outcome: policy.final_outcome,
        unknown_refs,
        limited_proof_refs,
        conflict_refs,
        redaction_limits,
        constraints,
    }
}

/// Build the ADR 0040 Cortex -> pai-axiom constraint envelope for a pack.
///
/// The envelope is stricter than the legacy [`AxiomContextExport`]: it carries
/// semantic trust, provenance, proof closure, contradiction, quarantine,
/// redaction, claim-language, and forbidden-use fields in one versioned shape.
#[must_use]
pub fn constraint_envelope_for_pack(pack: &ContextPack) -> CortexAxiomConstraintEnvelopeV1 {
    let export = axiom_export_for_pack(pack);
    let proof_report = proof_report_for_pack(pack);
    let provenance_class = weakest_provenance_for_pack(pack);
    let semantic_trust = weakest_semantic_trust_for_pack(pack);

    let mut envelope = CortexAxiomConstraintEnvelopeV1::new(
        pack.context_pack_id,
        proof_report,
        export.claim_ceiling,
        semantic_trust,
        provenance_class,
    );
    envelope.contradiction_state = contradiction_state_for_pack(pack);
    envelope.quarantine_state = quarantine_state_for_pack(pack, export.policy_outcome);
    envelope.redaction_state = redaction_state_for_pack(pack);
    envelope.allowed_claim_language =
        allowed_claim_language_for_quarantine(envelope.quarantine_state);
    envelope.constraints = export.constraints;
    envelope
}

fn pack_claim_ceiling(pack: &ContextPack) -> ClaimCeiling {
    let selected_ceiling = ClaimCeiling::weakest(pack.selected_refs.iter().map(|selected| {
        selected
            .claim_ceiling
            .min(selected.proof_state.claim_ceiling())
            .min(selected.runtime_mode.claim_ceiling())
            .min(selected.authority_class.claim_ceiling())
            .min(selected.provenance_class.claim_ceiling())
            .min(selected.semantic_trust.claim_ceiling())
    }))
    .unwrap_or(ClaimCeiling::DevOnly);

    if pack.conflicts.is_empty() {
        selected_ceiling
    } else {
        selected_ceiling.min(ClaimCeiling::DevOnly)
    }
}

fn proof_report_for_pack(pack: &ContextPack) -> ProofClosureReport {
    if pack
        .selected_refs
        .iter()
        .all(|selected| selected.proof_state == ClaimProofState::FullChainVerified)
    {
        return ProofClosureReport::full_chain_verified(Vec::new());
    }

    let mut failures = Vec::new();
    for selected in &pack.selected_refs {
        match selected.proof_state {
            ClaimProofState::FullChainVerified => {}
            ClaimProofState::Broken => failures.push(FailingEdge::broken(
                ProofEdgeKind::ContextPackLink,
                ref_to_string(&selected.ref_id),
                pack.context_pack_id.to_string(),
                ProofEdgeFailure::Mismatch,
                "selected context ref has broken proof state",
            )),
            ClaimProofState::Partial => failures.push(FailingEdge::unresolved(
                ProofEdgeKind::ContextPackLink,
                ref_to_string(&selected.ref_id),
                "selected context ref has partial proof state",
            )),
            ClaimProofState::Unknown => failures.push(FailingEdge::missing(
                ProofEdgeKind::ContextPackLink,
                ref_to_string(&selected.ref_id),
                "selected context ref has unknown proof state",
            )),
        }
    }

    ProofClosureReport::from_edges(Vec::new(), failures)
}

fn weakest_provenance_for_pack(pack: &ContextPack) -> ProvenanceClass {
    pack.selected_refs
        .iter()
        .map(|selected| selected.provenance_class)
        .min()
        .unwrap_or(ProvenanceClass::UnknownProvenance)
}

fn weakest_semantic_trust_for_pack(pack: &ContextPack) -> cortex_core::SemanticTrustClass {
    pack.selected_refs
        .iter()
        .map(|selected| selected.semantic_trust)
        .min()
        .unwrap_or(cortex_core::SemanticTrustClass::Unknown)
}

fn contradiction_state_for_pack(pack: &ContextPack) -> BoundaryContradictionState {
    pack.contradiction_posture()
}

fn quarantine_state_for_pack(
    pack: &ContextPack,
    policy_outcome: PolicyOutcome,
) -> BoundaryQuarantineState {
    match policy_outcome {
        PolicyOutcome::Reject => BoundaryQuarantineState::Contaminated,
        PolicyOutcome::Quarantine => BoundaryQuarantineState::Quarantined,
        PolicyOutcome::Allow | PolicyOutcome::Warn | PolicyOutcome::BreakGlass => {
            if pack.conflicts.is_empty() {
                BoundaryQuarantineState::Clean
            } else {
                BoundaryQuarantineState::DiagnosticOnly
            }
        }
    }
}

fn allowed_claim_language_for_quarantine(
    quarantine_state: BoundaryQuarantineState,
) -> Vec<AllowedClaimLanguage> {
    match quarantine_state {
        BoundaryQuarantineState::Clean | BoundaryQuarantineState::DiagnosticOnly => {
            cortex_core::default_allowed_claim_language()
        }
        BoundaryQuarantineState::Quarantined | BoundaryQuarantineState::Contaminated => vec![
            AllowedClaimLanguage::Constraint,
            AllowedClaimLanguage::ResidualRisk,
            AllowedClaimLanguage::VerificationRequest,
            AllowedClaimLanguage::Refusal,
        ],
    }
}

fn redaction_state_for_pack(pack: &ContextPack) -> BoundaryRedactionState {
    if pack.pack_mode == PackMode::Operator
        && pack.redaction_policy.raw_event_payloads == RawEventPayloadPolicy::OperatorOptIn
    {
        BoundaryRedactionState::RawOperatorOptIn
    } else if pack.redaction_policy.content == ContentRedaction::Abstracted {
        BoundaryRedactionState::Abstracted
    } else if pack.redaction_policy.raw_event_payloads == RawEventPayloadPolicy::Excluded {
        BoundaryRedactionState::Redacted
    } else {
        BoundaryRedactionState::ExportSafe
    }
}

fn redaction_limits(pack: &ContextPack) -> Vec<String> {
    let mut limits = Vec::new();
    if pack.redaction_policy.content == ContentRedaction::Abstracted {
        limits.push("content_abstracted".to_string());
    }
    if pack.redaction_policy.raw_event_payloads == RawEventPayloadPolicy::Excluded {
        limits.push("raw_event_payloads_excluded".to_string());
    }
    if !pack.exclusions.is_empty() {
        limits.push("explicit_exclusions_present".to_string());
    }
    limits
}

fn refs_to_strings(refs: &[ContextRefId]) -> Vec<String> {
    refs.iter().map(ref_to_string).collect()
}

fn ref_to_string(ref_id: &ContextRefId) -> String {
    match ref_id {
        ContextRefId::Memory { memory_id } => format!("memory:{memory_id}"),
        ContextRefId::Principle { principle_id } => format!("principle:{principle_id}"),
        ContextRefId::Event { event_id } => format!("event:{event_id}"),
    }
}

#[cfg(test)]
mod tests {
    use cortex_core::{
        AllowedClaimLanguage, AuthorityClass, BoundaryContradictionState, BoundaryQuarantineState,
        BoundaryRedactionState, ClaimCeiling, ClaimProofState, ContradictionId, EventId,
        ForbiddenBoundaryUse, ProvenanceClass, RuntimeMode, SemanticTrustClass,
        CORTEX_TO_AXIOM_CONSTRAINT_ENVELOPE_V1,
    };

    use super::*;
    use crate::{ContextPackBuilder, ContextRefCandidate, PackConflict, Sensitivity};

    fn event_ref() -> ContextRefId {
        ContextRefId::Event {
            event_id: EventId::new(),
        }
    }

    #[test]
    fn axiom_export_includes_truth_ceiling_and_unknowns() {
        let pack = ContextPackBuilder::new("prepare constrained AXIOM work", 512)
            .select_ref(ContextRefCandidate::new(
                event_ref(),
                "unverified candidate context",
            ))
            .build()
            .expect("build pack");

        let export = axiom_export_for_pack(&pack);

        assert_eq!(export.claim_ceiling, ClaimCeiling::DevOnly);
        assert_eq!(export.policy_outcome, PolicyOutcome::Allow);
        assert_eq!(export.unknown_refs.len(), 1);
        assert!(export
            .constraints
            .iter()
            .any(|constraint| constraint.kind == AxiomConstraintKind::TruthCeiling));
        assert!(export
            .constraints
            .iter()
            .any(|constraint| constraint.kind == AxiomConstraintKind::ProofStateLimit));
    }

    #[test]
    fn low_trust_conflicting_pack_forbids_promotion_shaped_output() {
        let ref_id = event_ref();
        let pack = ContextPackBuilder::new("prepare constrained AXIOM work", 512)
            .select_ref(
                ContextRefCandidate::new(ref_id.clone(), "conflicting candidate context")
                    .with_claim_metadata(
                        RuntimeMode::LocalUnsigned,
                        AuthorityClass::Derived,
                        ClaimProofState::Partial,
                        ClaimCeiling::AuthorityGrade,
                    ),
            )
            .conflict(PackConflict {
                contradiction_id: ContradictionId::new(),
                posture: BoundaryContradictionState::Blocked,
                refs: vec![ref_id],
                summary: "candidate context conflicts with another memory".to_string(),
            })
            .build()
            .expect("build pack");

        let export = axiom_export_for_pack(&pack);

        assert_eq!(export.claim_ceiling, ClaimCeiling::DevOnly);
        assert_eq!(export.policy_outcome, PolicyOutcome::Quarantine);
        assert!(!export.conflict_refs.is_empty());
        assert!(export.constraints.iter().any(|constraint| {
            constraint.kind == AxiomConstraintKind::ForbidPromotionShapedOutput
                && constraint.severity == AxiomConstraintSeverity::Hard
        }));
    }

    #[test]
    fn exported_constraints_do_not_grant_execution_authority() {
        let pack = ContextPackBuilder::new("prepare constrained AXIOM work", 512)
            .select_ref(
                ContextRefCandidate::new(event_ref(), "verified context")
                    .with_claim_metadata(
                        RuntimeMode::AuthorityGrade,
                        AuthorityClass::Operator,
                        ClaimProofState::FullChainVerified,
                        ClaimCeiling::AuthorityGrade,
                    )
                    .with_semantic_metadata(
                        ProvenanceClass::OperatorAttested,
                        SemanticTrustClass::FalsificationTested,
                    ),
            )
            .build()
            .expect("build pack");

        let export = axiom_export_for_pack(&pack);

        assert!(export.constraints.iter().any(|constraint| {
            constraint.kind == AxiomConstraintKind::NoExecutionAuthority
                && constraint.severity == AxiomConstraintSeverity::Hard
        }));
        assert_eq!(export.claim_ceiling, ClaimCeiling::AuthorityGrade);
    }

    #[test]
    fn redaction_policy_exports_boundary_constraints() {
        let pack = ContextPackBuilder::new("prepare constrained AXIOM work", 512)
            .select_ref(
                ContextRefCandidate::new(event_ref(), "private context")
                    .with_sensitivity(Sensitivity::Personal),
            )
            .build()
            .expect("build pack");

        let export = axiom_export_for_pack(&pack);

        assert!(export
            .redaction_limits
            .contains(&"raw_event_payloads_excluded".to_string()));
        assert!(export
            .constraints
            .iter()
            .any(|constraint| constraint.kind == AxiomConstraintKind::RedactionBoundary));
    }

    #[test]
    fn constraint_envelope_for_conflicted_pack_blocks_authority_uses() {
        let ref_id = event_ref();
        let pack = ContextPackBuilder::new("prepare constrained AXIOM work", 512)
            .select_ref(
                ContextRefCandidate::new(ref_id.clone(), "conflicted candidate context")
                    .with_claim_metadata(
                        RuntimeMode::LocalUnsigned,
                        AuthorityClass::Derived,
                        ClaimProofState::Partial,
                        ClaimCeiling::AuthorityGrade,
                    ),
            )
            .conflict(PackConflict {
                contradiction_id: ContradictionId::new(),
                posture: BoundaryContradictionState::MultiHypothesis,
                refs: vec![ref_id],
                summary: "candidate context conflicts with another memory".to_string(),
            })
            .build()
            .expect("build pack");

        let envelope = constraint_envelope_for_pack(&pack);

        assert_eq!(
            envelope.envelope_type,
            CORTEX_TO_AXIOM_CONSTRAINT_ENVELOPE_V1
        );
        assert_eq!(
            envelope.contradiction_state,
            BoundaryContradictionState::MultiHypothesis
        );
        assert_eq!(
            envelope.quarantine_state,
            BoundaryQuarantineState::Quarantined
        );
        assert_eq!(envelope.redaction_state, BoundaryRedactionState::Abstracted);
        assert_eq!(envelope.semantic_trust, SemanticTrustClass::CandidateOnly);
        assert_eq!(envelope.provenance_class, ProvenanceClass::RuntimeDerived);
        assert_eq!(envelope.truth_ceiling, ClaimCeiling::DevOnly);
        assert!(envelope
            .forbidden_uses
            .contains(&ForbiddenBoundaryUse::Promotion));
        assert!(envelope
            .forbidden_uses
            .contains(&ForbiddenBoundaryUse::TrustedHistory));
        assert!(envelope
            .forbidden_uses
            .contains(&ForbiddenBoundaryUse::Release));
        assert!(!envelope
            .allowed_claim_language
            .contains(&AllowedClaimLanguage::CandidateClaim));
        assert!(!envelope
            .allowed_claim_language
            .contains(&AllowedClaimLanguage::EvidenceReference));
        assert!(envelope
            .allowed_claim_language
            .contains(&AllowedClaimLanguage::VerificationRequest));
        assert!(envelope
            .allowed_claim_language
            .contains(&AllowedClaimLanguage::Refusal));
    }

    #[test]
    fn constraint_envelope_carries_operator_raw_redaction_state() {
        let pack = ContextPackBuilder::new("prepare operator-only AXIOM work", 512)
            .pack_mode(crate::PackMode::Operator)
            .include_raw_event_payloads_in_operator_mode()
            .select_ref(
                ContextRefCandidate::new(event_ref(), "operator raw context")
                    .with_sensitivity(Sensitivity::Internal)
                    .with_raw_event_payload(serde_json::json!({"payload": "operator local"}))
                    .with_claim_metadata(
                        RuntimeMode::AuthorityGrade,
                        AuthorityClass::Operator,
                        ClaimProofState::FullChainVerified,
                        ClaimCeiling::AuthorityGrade,
                    )
                    .with_semantic_metadata(
                        ProvenanceClass::OperatorAttested,
                        SemanticTrustClass::FalsificationTested,
                    ),
            )
            .build()
            .expect("build pack");

        let envelope = constraint_envelope_for_pack(&pack);

        assert_eq!(
            envelope.redaction_state,
            BoundaryRedactionState::RawOperatorOptIn
        );
        assert_eq!(envelope.provenance_class, ProvenanceClass::OperatorAttested);
        assert_eq!(
            envelope.semantic_trust,
            SemanticTrustClass::FalsificationTested
        );
    }

    #[test]
    fn rejected_pack_allows_only_diagnostic_claim_language() {
        let mut pack = ContextPackBuilder::new("prepare rejected AXIOM work", 512)
            .select_ref(
                ContextRefCandidate::new(event_ref(), "raw external context")
                    .with_raw_event_payload(serde_json::json!({"payload": "must not leak"})),
            )
            .build()
            .expect("build pack");
        pack.redaction_policy.raw_event_payloads = RawEventPayloadPolicy::OperatorOptIn;

        let envelope = constraint_envelope_for_pack(&pack);

        assert_eq!(
            envelope.quarantine_state,
            BoundaryQuarantineState::Contaminated
        );
        assert!(!envelope
            .allowed_claim_language
            .contains(&AllowedClaimLanguage::CandidateClaim));
        assert!(!envelope
            .allowed_claim_language
            .contains(&AllowedClaimLanguage::EvidenceReference));
        assert!(envelope
            .allowed_claim_language
            .contains(&AllowedClaimLanguage::Constraint));
        assert!(envelope
            .allowed_claim_language
            .contains(&AllowedClaimLanguage::ResidualRisk));
        assert!(envelope
            .allowed_claim_language
            .contains(&AllowedClaimLanguage::VerificationRequest));
        assert!(envelope
            .allowed_claim_language
            .contains(&AllowedClaimLanguage::Refusal));
    }
}
