//! Escalation events for high-conflict assertions.
//!
//! When the Materializer detects a conflict score exceeding a configured threshold,
//! it records an EscalationEvent. External systems (research agents, human reviewers)
//! can poll for pending escalations and resolve them.

use rkyv::{Archive, Deserialize, Serialize};

/// Severity level for an escalation event.
///
/// Determines the urgency and required response for high-conflict assertions.
#[derive(Archive, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[archive(check_bytes)]
pub enum EscalationLevel {
    /// Informational: flag for analytics, no action required.
    Low,
    /// Medium priority: flag for review queue.
    Medium,
    /// High priority: block dependent actions, require approval.
    High,
    /// Critical: immediate human intervention required.
    Critical,
}

/// An escalation event triggered by high conflict during materialization.
///
/// Stored at `ESC:{timestamp_nanos}:{id_hex}` to enable efficient time-range queries.
/// The ID is the BLAKE3 hash of the event content for content-addressing.
#[derive(Archive, Deserialize, Serialize, Debug, Clone, PartialEq)]
#[archive(check_bytes)]
pub struct EscalationEvent {
    /// Content-addressed ID: BLAKE3 hash of (subject, predicate, conflict_score, timestamp).
    pub id: [u8; 32],
    /// Subject of the conflicting assertion pair.
    pub subject: String,
    /// Predicate of the conflicting assertion pair.
    pub predicate: String,
    /// The conflict score that triggered this escalation (0.0 to 1.0).
    pub conflict_score: f32,
    /// Severity level assigned by the policy.
    pub level: EscalationLevel,
    /// Human-readable description of why this escalation was triggered.
    pub reason: String,
    /// Unix timestamp (nanoseconds) when this event was created.
    pub timestamp: u64,
    /// Has this escalation been acknowledged/resolved?
    pub resolved: bool,
}

impl EscalationEvent {
    /// Compute the content-addressed ID for this event.
    ///
    /// ID = BLAKE3(subject || predicate || conflict_score || timestamp)
    pub fn compute_id(
        subject: &str,
        predicate: &str,
        conflict_score: f32,
        timestamp: u64,
    ) -> [u8; 32] {
        let mut hasher = blake3::Hasher::new();
        hasher.update(subject.as_bytes());
        hasher.update(predicate.as_bytes());
        hasher.update(&conflict_score.to_le_bytes());
        hasher.update(&timestamp.to_le_bytes());
        *hasher.finalize().as_bytes()
    }
}

/// A policy that defines when to trigger escalations.
///
/// Configured at startup and checked during materialization. When a
/// conflict score exceeds the threshold (and predicate matches if specified),
/// an EscalationEvent is written.
#[derive(Debug, Clone, PartialEq)]
pub struct EscalationPolicy {
    /// Human-readable name for this policy (e.g., "HighConflictClinical").
    pub name: String,
    /// Trigger when conflict_score >= this threshold (0.0 to 1.0).
    pub min_conflict_score: f32,
    /// Severity level to assign to triggered events.
    pub level: EscalationLevel,
    /// Optional predicate pattern: only trigger for matching predicates.
    /// If None, applies to all predicates.
    pub predicate_pattern: Option<String>,
}

impl EscalationPolicy {
    /// Check if this policy applies to the given predicate.
    ///
    /// If `predicate_pattern` is None, matches all predicates.
    /// Otherwise, checks if the predicate contains the pattern.
    pub fn matches_predicate(&self, predicate: &str) -> bool {
        match &self.predicate_pattern {
            Some(pattern) => predicate.contains(pattern),
            None => true,
        }
    }

    /// Check if this policy should trigger for the given conflict score and predicate.
    pub fn should_trigger(&self, conflict_score: f32, predicate: &str) -> bool {
        conflict_score >= self.min_conflict_score && self.matches_predicate(predicate)
    }
}

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

    #[test]
    fn test_escalation_level_serialization_roundtrip() {
        let levels = [
            EscalationLevel::Low,
            EscalationLevel::Medium,
            EscalationLevel::High,
            EscalationLevel::Critical,
        ];

        for level in levels {
            let event = EscalationEvent {
                id: [0u8; 32],
                subject: "Test".to_string(),
                predicate: "test".to_string(),
                conflict_score: 0.8,
                level,
                reason: "test".to_string(),
                timestamp: 1000,
                resolved: false,
            };

            let bytes = serde::serialize(&event).expect("serialize");
            let restored: EscalationEvent = serde::deserialize(&bytes).expect("deserialize");

            assert_eq!(restored.level, level);
        }
    }

    #[test]
    fn test_escalation_event_id_deterministic() {
        let id1 = EscalationEvent::compute_id("Tesla", "revenue", 0.85, 1000);
        let id2 = EscalationEvent::compute_id("Tesla", "revenue", 0.85, 1000);
        assert_eq!(id1, id2, "Same inputs should produce same ID");

        let id3 = EscalationEvent::compute_id("Tesla", "revenue", 0.86, 1000);
        assert_ne!(id1, id3, "Different conflict_score should produce different ID");

        let id4 = EscalationEvent::compute_id("Tesla", "profit", 0.85, 1000);
        assert_ne!(id1, id4, "Different predicate should produce different ID");
    }

    #[test]
    fn test_policy_matches_predicate() {
        let global_policy = EscalationPolicy {
            name: "Global".to_string(),
            min_conflict_score: 0.7,
            level: EscalationLevel::Medium,
            predicate_pattern: None,
        };

        assert!(global_policy.matches_predicate("revenue"));
        assert!(global_policy.matches_predicate("treats_condition"));
        assert!(global_policy.matches_predicate("anything"));

        let clinical_policy = EscalationPolicy {
            name: "Clinical".to_string(),
            min_conflict_score: 0.5,
            level: EscalationLevel::High,
            predicate_pattern: Some("treats".to_string()),
        };

        assert!(clinical_policy.matches_predicate("treats_condition"));
        assert!(clinical_policy.matches_predicate("contraindicated_treats"));
        assert!(!clinical_policy.matches_predicate("revenue"));
    }

    #[test]
    fn test_policy_should_trigger() {
        let policy = EscalationPolicy {
            name: "Test".to_string(),
            min_conflict_score: 0.7,
            level: EscalationLevel::Medium,
            predicate_pattern: Some("clinical".to_string()),
        };

        // Should trigger: high conflict, matching predicate
        assert!(policy.should_trigger(0.8, "clinical_outcome"));

        // Should NOT trigger: low conflict
        assert!(!policy.should_trigger(0.6, "clinical_outcome"));

        // Should NOT trigger: non-matching predicate
        assert!(!policy.should_trigger(0.8, "revenue"));

        // Should NOT trigger: both wrong
        assert!(!policy.should_trigger(0.6, "revenue"));
    }
}
