//! Policy evaluation for session signal writes.

use std::sync::atomic::Ordering;
use std::time::Instant;

use crate::schema::AgentPolicy;

use super::state::SessionState;

// ── PolicyViolationKind ───────────────────────────────────────────────────────

/// Typed reason category for a policy check failure.
///
/// Allows callers to dispatch on the specific cause without parsing strings.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PolicyViolationKind {
    /// Session duration limit exceeded.
    Expired,
    /// Per-session signal count cap reached.
    CountCap,
    /// Signal type is in the `denied_signals` list.
    Denied,
    /// Signal type is not in the non-empty `allowed_signals` list.
    NotAllowed,
}

// ── PolicyViolation ───────────────────────────────────────────────────────────

/// Describes why a policy check rejected a signal write.
#[derive(Debug, Clone)]
pub struct PolicyViolation {
    /// Typed reason for the rejection — used for error-type dispatch.
    pub kind: PolicyViolationKind,
    pub signal_type: String,
    pub policy_name: String,
    pub reason: String,
}

// ── PolicyEvaluator ───────────────────────────────────────────────────────────

/// Evaluates policy rules against incoming session signals.
pub struct PolicyEvaluator<'a> {
    policy: &'a AgentPolicy,
    policy_name: &'a str,
}

impl<'a> PolicyEvaluator<'a> {
    #[must_use]
    pub const fn new(policy: &'a AgentPolicy, policy_name: &'a str) -> Self {
        Self {
            policy,
            policy_name,
        }
    }

    /// Check whether a signal can be written under this policy.
    ///
    /// Returns `Ok(())` if all policy checks pass.
    ///
    /// # Errors
    ///
    /// Returns `PolicyViolation` describing the first rule violated.
    pub fn check(
        &self,
        signal_type: &str,
        state: &SessionState,
        now: Instant,
    ) -> Result<(), PolicyViolation> {
        let make_violation = |kind: PolicyViolationKind, reason: String| PolicyViolation {
            kind,
            signal_type: signal_type.to_owned(),
            policy_name: self.policy_name.to_owned(),
            reason,
        };

        // 1. Duration check.
        let elapsed = now.duration_since(state.started_at);
        if elapsed > self.policy.max_session_duration {
            return Err(make_violation(
                PolicyViolationKind::Expired,
                format!(
                    "session expired after {:.1}s (max {:.1}s)",
                    elapsed.as_secs_f64(),
                    self.policy.max_session_duration.as_secs_f64()
                ),
            ));
        }

        // 2. Count cap (0 = unlimited).
        if self.policy.max_signals_per_session > 0
            && state.signals_written.load(Ordering::Relaxed)
                >= u64::from(self.policy.max_signals_per_session)
        {
            return Err(make_violation(
                PolicyViolationKind::CountCap,
                format!(
                    "signal count cap reached (max {})",
                    self.policy.max_signals_per_session
                ),
            ));
        }

        // 3. Deny list.
        if self
            .policy
            .denied_signals
            .iter()
            .any(|s| s.as_str() == signal_type)
        {
            return Err(make_violation(
                PolicyViolationKind::Denied,
                format!(
                    "signal '{signal_type}' is explicitly denied by policy '{}'",
                    self.policy_name
                ),
            ));
        }

        // 4. Allow list (empty allow list = all allowed).
        if !self.policy.allowed_signals.is_empty()
            && !self
                .policy
                .allowed_signals
                .iter()
                .any(|s| s.as_str() == signal_type)
        {
            return Err(make_violation(
                PolicyViolationKind::NotAllowed,
                format!(
                    "signal '{signal_type}' not in allowed_signals for policy '{}'",
                    self.policy_name
                ),
            ));
        }

        Ok(())
    }
}

// ── Unit tests ────────────────────────────────────────────────────────────────

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use std::collections::HashMap;
    use std::sync::atomic::{AtomicBool, AtomicU64};
    use std::sync::{Arc, Mutex};
    use std::time::Duration;

    use dashmap::DashMap;

    use super::super::audit::AuditLog;
    use super::super::types::{AgentId, SessionId};

    fn make_state(policy_name: &str) -> SessionState {
        SessionState {
            id: SessionId(1),
            user_id: 100,
            agent_id: AgentId::new("test-agent").unwrap(),
            policy_name: policy_name.to_owned(),
            started_at: Instant::now(),
            started_at_ns: 0,
            metadata: HashMap::new(),
            signals: DashMap::new(),
            signaled_entities: DashMap::new(),
            annotations: Mutex::new(Vec::new()),
            signals_written: AtomicU64::new(0),
            signals_rejected: AtomicU64::new(0),
            audit_log: Mutex::new(AuditLog::new()),
            closed: Arc::new(AtomicBool::new(false)),
        }
    }

    #[test]
    fn policy_allow_list_accepts_allowed_signal() {
        let policy = AgentPolicy {
            allowed_signals: vec!["view".to_string()],
            denied_signals: vec![],
            max_session_duration: Duration::from_secs(3600),
            max_signals_per_session: 0,
        };
        let evaluator = PolicyEvaluator::new(&policy, "test_policy");
        let state = make_state("test_policy");
        assert!(evaluator.check("view", &state, Instant::now()).is_ok());
    }

    #[test]
    fn policy_allow_list_rejects_unknown_signal() {
        let policy = AgentPolicy {
            allowed_signals: vec!["view".to_string()],
            denied_signals: vec![],
            max_session_duration: Duration::from_secs(3600),
            max_signals_per_session: 0,
        };
        let evaluator = PolicyEvaluator::new(&policy, "test_policy");
        let state = make_state("test_policy");
        assert!(evaluator.check("like", &state, Instant::now()).is_err());
    }

    #[test]
    fn policy_deny_list_rejects_denied_signal() {
        let policy = AgentPolicy {
            allowed_signals: vec![],
            denied_signals: vec!["block".to_string()],
            max_session_duration: Duration::from_secs(3600),
            max_signals_per_session: 0,
        };
        let evaluator = PolicyEvaluator::new(&policy, "test_policy");
        let state = make_state("test_policy");
        assert!(evaluator.check("block", &state, Instant::now()).is_err());
    }

    #[test]
    fn policy_count_cap_rejects_when_exceeded() {
        let policy = AgentPolicy {
            allowed_signals: vec![],
            denied_signals: vec![],
            max_session_duration: Duration::from_secs(3600),
            max_signals_per_session: 2,
        };
        let evaluator = PolicyEvaluator::new(&policy, "test_policy");
        let state = make_state("test_policy");
        state.signals_written.store(2, Ordering::Relaxed);
        assert!(evaluator.check("view", &state, Instant::now()).is_err());
    }

    #[test]
    fn policy_expired_session_rejected() {
        let policy = AgentPolicy {
            allowed_signals: vec![],
            denied_signals: vec![],
            max_session_duration: Duration::from_millis(1),
            max_signals_per_session: 0,
        };
        let evaluator = PolicyEvaluator::new(&policy, "test_policy");
        let state = SessionState {
            id: SessionId(1),
            user_id: 100,
            agent_id: AgentId::new("test-agent").unwrap(),
            policy_name: "test_policy".to_owned(),
            // started_at far in the past:
            started_at: Instant::now() - Duration::from_secs(10),
            started_at_ns: 0,
            metadata: HashMap::new(),
            signals: DashMap::new(),
            signaled_entities: DashMap::new(),
            annotations: Mutex::new(Vec::new()),
            signals_written: AtomicU64::new(0),
            signals_rejected: AtomicU64::new(0),
            audit_log: Mutex::new(AuditLog::new()),
            closed: Arc::new(AtomicBool::new(false)),
        };
        assert!(evaluator.check("view", &state, Instant::now()).is_err());
    }
}
