//! Policy outcome lattice and deterministic composition.
//!
//! ADR 0026 defines one total order for allow/warn/reject/quarantine/break-glass
//! decisions. This module is pure shape logic: subsystems register rule ids,
//! submit rule outcomes, and receive the composed decision with explainability.

use std::collections::BTreeSet;

use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::claims::ClaimCeiling;

/// Policy outcome total order from weakest to strongest.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum PolicyOutcome {
    /// Proceed.
    Allow,
    /// Proceed with a structured warning.
    Warn,
    /// Proceed only under explicit break-glass scope and attestation.
    BreakGlass,
    /// Persist only isolated unsafe state.
    Quarantine,
    /// Hard stop.
    Reject,
}

impl PolicyOutcome {
    /// Maximum claim ceiling permitted by this policy outcome.
    #[must_use]
    pub const fn claim_ceiling(self) -> ClaimCeiling {
        match self {
            Self::Allow | Self::Warn | Self::BreakGlass => ClaimCeiling::AuthorityGrade,
            Self::Quarantine | Self::Reject => ClaimCeiling::DevOnly,
        }
    }
}

/// A registered policy rule id.
#[derive(
    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
pub struct PolicyRuleId(String);

impl PolicyRuleId {
    /// Create a policy rule id.
    ///
    /// Empty or whitespace-only ids are rejected because explainability output
    /// must be machine-correlatable.
    pub fn new(value: impl Into<String>) -> Result<Self, PolicyError> {
        let value = value.into();
        if value.trim().is_empty() {
            return Err(PolicyError::EmptyRuleId);
        }
        Ok(Self(value))
    }

    /// Borrow the rule id as a string.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

/// One rule's contribution to a composed policy decision.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PolicyContribution {
    /// Rule id that emitted this contribution.
    pub rule_id: PolicyRuleId,
    /// Outcome emitted by the rule.
    pub outcome: PolicyOutcome,
    /// Stable operator-facing reason.
    pub reason: String,
    /// Whether this rule permits a scoped break-glass override when it returns
    /// `Reject` or `Quarantine`.
    pub break_glass_override_allowed: bool,
}

impl PolicyContribution {
    /// Construct a contribution.
    pub fn new(
        rule_id: impl Into<String>,
        outcome: PolicyOutcome,
        reason: impl Into<String>,
    ) -> Result<Self, PolicyError> {
        let reason = reason.into();
        if reason.trim().is_empty() {
            return Err(PolicyError::EmptyReason);
        }
        Ok(Self {
            rule_id: PolicyRuleId::new(rule_id)?,
            outcome,
            reason,
            break_glass_override_allowed: false,
        })
    }

    /// Mark this contribution as eligible for explicit break-glass override.
    #[must_use]
    pub const fn allow_break_glass_override(mut self) -> Self {
        self.break_glass_override_allowed = true;
        self
    }
}

/// Closed break-glass reason code.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum BreakGlassReasonCode {
    /// Incident response or containment work.
    IncidentResponse,
    /// Backup, restore, or disaster recovery work.
    RestoreRecovery,
    /// Explicit operator correction of known bad state.
    OperatorCorrection,
    /// Bounded migration or cutover work.
    DataMigration,
    /// Diagnostic-only persistence of an otherwise rejected or quarantined
    /// artifact (ADR 0026 §5; `BoundaryQuarantineState::DiagnosticOnly`).
    /// The persisted artifact MUST carry `forbidden_uses` and MUST NOT
    /// promote to default trusted history.
    DiagnosticOnly,
}

/// Scope bound to a break-glass action.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct BreakGlassScope {
    /// Registered operation type.
    pub operation_type: String,
    /// Artifact ids or refs covered by this authorization.
    pub artifact_refs: Vec<String>,
    /// Optional validity start.
    pub not_before: Option<DateTime<Utc>>,
    /// Optional validity end.
    pub not_after: Option<DateTime<Utc>>,
}

impl BreakGlassScope {
    /// Whether the scope has enough information to bind an authorization.
    #[must_use]
    pub fn is_bound(&self) -> bool {
        !self.operation_type.trim().is_empty()
            && !self.artifact_refs.is_empty()
            && self
                .artifact_refs
                .iter()
                .all(|artifact_ref| !artifact_ref.trim().is_empty())
            && self
                .not_before
                .zip(self.not_after)
                .is_none_or(|(not_before, not_after)| not_before <= not_after)
    }
}

/// Explicit authorization for a break-glass override.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct BreakGlassAuthorization {
    /// Whether the governing rule set permits break-glass for this operation.
    pub permitted: bool,
    /// Whether required attestation was verified.
    pub attested: bool,
    /// Scope bound to this break-glass action.
    pub scope: BreakGlassScope,
    /// Closed reason code.
    pub reason_code: BreakGlassReasonCode,
}

impl BreakGlassAuthorization {
    /// Whether this authorization may turn a reject/quarantine composition into
    /// a break-glass decision.
    #[must_use]
    pub fn is_valid(&self) -> bool {
        self.permitted && self.attested && self.scope.is_bound()
    }
}

/// Audit shape required when break-glass is the final policy outcome.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct BreakGlassAuditShape {
    /// Scope bound to the break-glass decision.
    pub scope: BreakGlassScope,
    /// Closed reason code.
    pub reason_code: BreakGlassReasonCode,
    /// Rule outcomes that contributed to the composed decision.
    pub contributing_outcomes: Vec<PolicyContribution>,
    /// Optional authorization expiry copied from scope.
    pub expires_at: Option<DateTime<Utc>>,
}

impl BreakGlassAuditShape {
    /// Build the mandatory audit shape from a break-glass decision.
    #[must_use]
    pub fn from_decision(decision: &PolicyDecision) -> Option<Self> {
        let authorization = decision.break_glass.as_ref()?;
        if decision.final_outcome != PolicyOutcome::BreakGlass || !authorization.is_valid() {
            return None;
        }
        Some(Self {
            scope: authorization.scope.clone(),
            reason_code: authorization.reason_code,
            contributing_outcomes: decision.contributing.clone(),
            expires_at: authorization.scope.not_after,
        })
    }
}

/// Composed policy decision with explainability.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PolicyDecision {
    /// Final composed outcome.
    pub final_outcome: PolicyOutcome,
    /// Contributions that determined the final outcome.
    pub contributing: Vec<PolicyContribution>,
    /// Outcomes discarded by composition.
    pub discarded: Vec<PolicyContribution>,
    /// Break-glass authorization used, if any.
    pub break_glass: Option<BreakGlassAuthorization>,
}

/// Pure policy engine with registered rule ids.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PolicyEngine {
    registered_rules: BTreeSet<PolicyRuleId>,
}

impl PolicyEngine {
    /// Create an empty policy engine.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Register a rule id.
    pub fn register_rule(&mut self, rule_id: impl Into<String>) -> Result<(), PolicyError> {
        self.registered_rules.insert(PolicyRuleId::new(rule_id)?);
        Ok(())
    }

    /// Compose outcomes for one artifact or operation.
    pub fn compose(
        &self,
        contributions: Vec<PolicyContribution>,
        break_glass: Option<BreakGlassAuthorization>,
    ) -> Result<PolicyDecision, PolicyError> {
        if contributions.is_empty() {
            return Err(PolicyError::NoContributions);
        }

        for contribution in &contributions {
            if !self.registered_rules.contains(&contribution.rule_id) {
                return Err(PolicyError::UnregisteredRule(
                    contribution.rule_id.as_str().to_string(),
                ));
            }
        }

        Ok(compose_policy_outcomes(contributions, break_glass))
    }
}

/// Compose policy outcomes without a registry check.
#[must_use]
pub fn compose_policy_outcomes(
    mut contributions: Vec<PolicyContribution>,
    break_glass: Option<BreakGlassAuthorization>,
) -> PolicyDecision {
    contributions.sort_by(|a, b| {
        b.outcome
            .cmp(&a.outcome)
            .then_with(|| a.rule_id.cmp(&b.rule_id))
    });

    let strongest = contributions
        .first()
        .map(|contribution| contribution.outcome)
        .unwrap_or(PolicyOutcome::Allow);
    let has_break_glass = contributions
        .iter()
        .any(|contribution| contribution.outcome == PolicyOutcome::BreakGlass);
    let every_blocking_rule_allows_break_glass = contributions
        .iter()
        .filter(|contribution| {
            matches!(
                contribution.outcome,
                PolicyOutcome::Reject | PolicyOutcome::Quarantine
            )
        })
        .all(|contribution| contribution.break_glass_override_allowed);
    let break_glass_valid = break_glass
        .as_ref()
        .is_some_and(BreakGlassAuthorization::is_valid);
    let final_outcome = if matches!(strongest, PolicyOutcome::Reject | PolicyOutcome::Quarantine)
        && has_break_glass
        && every_blocking_rule_allows_break_glass
        && break_glass_valid
    {
        PolicyOutcome::BreakGlass
    } else {
        strongest
    };

    let (contributing, discarded) = contributions
        .into_iter()
        .partition(|contribution| contribution.outcome == final_outcome);

    PolicyDecision {
        final_outcome,
        contributing,
        discarded,
        break_glass: break_glass.filter(BreakGlassAuthorization::is_valid),
    }
}

/// Policy composition error.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PolicyError {
    /// Rule id was empty.
    EmptyRuleId,
    /// Contribution reason was empty.
    EmptyReason,
    /// No contributions were supplied.
    NoContributions,
    /// A contribution used an unregistered rule id.
    UnregisteredRule(String),
}

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

    fn contribution(rule_id: &str, outcome: PolicyOutcome) -> PolicyContribution {
        PolicyContribution::new(rule_id, outcome, format!("{rule_id} reason")).unwrap()
    }

    fn break_glassable_contribution(rule_id: &str, outcome: PolicyOutcome) -> PolicyContribution {
        contribution(rule_id, outcome).allow_break_glass_override()
    }

    fn break_glass_scope() -> BreakGlassScope {
        BreakGlassScope {
            operation_type: "memory.override".into(),
            artifact_refs: vec!["mem_01".into()],
            not_before: None,
            not_after: None,
        }
    }

    #[test]
    fn policy_outcome_order_matches_adr_0026() {
        assert!(PolicyOutcome::Reject > PolicyOutcome::Quarantine);
        assert!(PolicyOutcome::Quarantine > PolicyOutcome::BreakGlass);
        assert!(PolicyOutcome::BreakGlass > PolicyOutcome::Warn);
        assert!(PolicyOutcome::Warn > PolicyOutcome::Allow);
    }

    #[test]
    fn reject_and_quarantine_cap_claims_to_dev_only() {
        assert_eq!(PolicyOutcome::Reject.claim_ceiling(), ClaimCeiling::DevOnly);
        assert_eq!(
            PolicyOutcome::Quarantine.claim_ceiling(),
            ClaimCeiling::DevOnly
        );
        assert_eq!(
            PolicyOutcome::BreakGlass.claim_ceiling(),
            ClaimCeiling::AuthorityGrade
        );
    }

    #[test]
    fn policy_engine_rejects_unregistered_rules() {
        let engine = PolicyEngine::new();
        let err = engine
            .compose(
                vec![contribution("memory.closure", PolicyOutcome::Reject)],
                None,
            )
            .unwrap_err();

        assert_eq!(err, PolicyError::UnregisteredRule("memory.closure".into()));
    }

    #[test]
    fn strongest_outcome_wins_with_deterministic_explainability() {
        let mut engine = PolicyEngine::new();
        engine.register_rule("pack.strict").unwrap();
        engine.register_rule("memory.closure").unwrap();
        engine.register_rule("tier.warn").unwrap();

        let decision = engine
            .compose(
                vec![
                    contribution("tier.warn", PolicyOutcome::Warn),
                    contribution("pack.strict", PolicyOutcome::Reject),
                    contribution("memory.closure", PolicyOutcome::Reject),
                ],
                None,
            )
            .unwrap();

        assert_eq!(decision.final_outcome, PolicyOutcome::Reject);
        assert_eq!(decision.contributing.len(), 2);
        assert_eq!(decision.contributing[0].rule_id.as_str(), "memory.closure");
        assert_eq!(decision.contributing[1].rule_id.as_str(), "pack.strict");
        assert_eq!(decision.discarded[0].outcome, PolicyOutcome::Warn);
    }

    #[test]
    fn break_glass_cannot_override_without_attestation() {
        let decision = compose_policy_outcomes(
            vec![
                contribution("memory.closure", PolicyOutcome::Reject),
                contribution("operator.override", PolicyOutcome::BreakGlass),
            ],
            Some(BreakGlassAuthorization {
                permitted: true,
                attested: false,
                scope: break_glass_scope(),
                reason_code: BreakGlassReasonCode::OperatorCorrection,
            }),
        );

        assert_eq!(decision.final_outcome, PolicyOutcome::Reject);
        assert!(decision.break_glass.is_none());
    }

    #[test]
    fn valid_break_glass_can_be_final_when_explicitly_authorized() {
        let decision = compose_policy_outcomes(
            vec![
                break_glassable_contribution("memory.closure", PolicyOutcome::Quarantine),
                contribution("operator.override", PolicyOutcome::BreakGlass),
            ],
            Some(BreakGlassAuthorization {
                permitted: true,
                attested: true,
                scope: break_glass_scope(),
                reason_code: BreakGlassReasonCode::IncidentResponse,
            }),
        );

        assert_eq!(decision.final_outcome, PolicyOutcome::BreakGlass);
        assert_eq!(decision.contributing.len(), 1);
        assert_eq!(decision.contributing[0].outcome, PolicyOutcome::BreakGlass);
        assert!(decision.break_glass.is_some());
        let audit_shape = BreakGlassAuditShape::from_decision(&decision).unwrap();
        assert_eq!(
            audit_shape.reason_code,
            BreakGlassReasonCode::IncidentResponse
        );
        assert_eq!(audit_shape.scope.operation_type, "memory.override");
    }

    #[test]
    fn break_glass_cannot_bypass_non_overridable_reject() {
        for rule_id in [
            "actor.attestation.0010",
            "canonical.hash.0022",
            "trust.tier.0019",
        ] {
            let decision = compose_policy_outcomes(
                vec![
                    contribution(rule_id, PolicyOutcome::Reject),
                    break_glassable_contribution("operator.override", PolicyOutcome::BreakGlass),
                ],
                Some(BreakGlassAuthorization {
                    permitted: true,
                    attested: true,
                    scope: break_glass_scope(),
                    reason_code: BreakGlassReasonCode::IncidentResponse,
                }),
            );

            assert_eq!(
                decision.final_outcome,
                PolicyOutcome::Reject,
                "{rule_id} must remain non-overridable"
            );
            assert!(decision.break_glass.is_some());
            assert!(BreakGlassAuditShape::from_decision(&decision).is_none());
        }
    }

    #[test]
    fn break_glass_scope_requires_operation_and_artifact_refs() {
        let mut scope = break_glass_scope();
        assert!(scope.is_bound());

        scope.artifact_refs.clear();
        assert!(!scope.is_bound());

        scope.artifact_refs.push("mem_01".into());
        scope.operation_type.clear();
        assert!(!scope.is_bound());
    }
}
