//! Bounded audit log for session policy decisions.

use std::collections::VecDeque;

/// Maximum audit log entries before truncation.
pub const MAX_AUDIT_ENTRIES: usize = 10_000;
/// Maximum annotation entries.
pub const MAX_ANNOTATIONS: usize = 100;
/// Maximum number of closed sessions retained in memory.
pub const MAX_CLOSED_SESSIONS: usize = 10_000;
/// Number of oldest closed sessions to evict when the cap is exceeded.
pub const EVICT_BATCH_SIZE: usize = 1_000;
/// Hard structural cap on distinct entity IDs tracked per session.
///
/// The `signaled_entities` set feeds the FOR SESSION entity-level boost. It is
/// bounded independently of `Policy::max_signals_per_session` (which may be `0`
/// / unbounded) so a long-lived or adversarial session cannot grow the set
/// without limit. Once the cap is reached, further distinct entities are
/// dropped rather than evicted — the boost degrades gracefully to the first
/// `MAX_SIGNALED_ENTITIES` entities seen.
pub const MAX_SIGNALED_ENTITIES: usize = 100_000;

// ── AuditEntry ────────────────────────────────────────────────────────────────

/// A single policy check result recorded in the session audit log.
#[derive(Debug, Clone)]
pub struct AuditEntry {
    /// Nanosecond timestamp of the check.
    pub timestamp_ns: u64,
    /// Signal type name that was checked.
    pub signal_type: String,
    /// Whether the signal was accepted.
    pub accepted: bool,
    /// Reason for rejection (present when `accepted == false`).
    pub reason: Option<String>,
}

// ── AuditLog ──────────────────────────────────────────────────────────────────

/// Bounded audit log with overflow eviction.
///
/// Holds up to `MAX_AUDIT_ENTRIES` entries. When the cap is reached, the oldest
/// entry is evicted and `truncated` is set to `true`.
///
/// Backed by a [`VecDeque`] so overflow eviction is O(1) front-pop — a `Vec`
/// with `remove(0)` was O(n) on *every* push past the cap, shifting the whole
/// buffer per accepted signal on a hot session.
pub struct AuditLog {
    entries: VecDeque<AuditEntry>,
    /// Set to `true` once an entry has been evicted due to overflow.
    pub truncated: bool,
}

impl AuditLog {
    /// Create an empty audit log.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            entries: VecDeque::new(),
            truncated: false,
        }
    }

    /// Append an entry, evicting the oldest if the cap is reached.
    ///
    /// Eviction is O(1): the oldest entry is popped from the front of the
    /// `VecDeque` rather than shifting the entire backing buffer.
    pub fn push(&mut self, entry: AuditEntry) {
        if self.entries.len() >= MAX_AUDIT_ENTRIES {
            self.entries.pop_front();
            self.truncated = true;
        }
        self.entries.push_back(entry);
    }

    /// Snapshot the entries in oldest-to-newest order.
    ///
    /// Returns an owned `Vec` rather than a borrowed slice: a `VecDeque` is not
    /// guaranteed contiguous, so a borrow-free copy is the cheapest stable view.
    /// Both callers (snapshot archival and `session_audit`) want an owned copy,
    /// and audit retrieval is a cold path.
    #[must_use]
    pub fn entries(&self) -> Vec<AuditEntry> {
        self.entries.iter().cloned().collect()
    }

    /// Number of entries currently held.
    #[must_use]
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// `true` if no entries are held.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

impl Default for AuditLog {
    fn default() -> Self {
        Self::new()
    }
}

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

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

    #[test]
    fn audit_log_truncates_on_overflow() {
        let mut log = AuditLog::new();
        assert!(!log.truncated);

        // Fill to capacity.
        for i in 0..MAX_AUDIT_ENTRIES {
            log.push(AuditEntry {
                timestamp_ns: i as u64,
                signal_type: "view".to_string(),
                accepted: true,
                reason: None,
            });
        }
        assert!(!log.truncated);
        assert_eq!(log.len(), MAX_AUDIT_ENTRIES);

        // One more should evict oldest and set truncated.
        log.push(AuditEntry {
            timestamp_ns: MAX_AUDIT_ENTRIES as u64,
            signal_type: "view".to_string(),
            accepted: true,
            reason: None,
        });
        assert!(log.truncated);
        assert_eq!(log.len(), MAX_AUDIT_ENTRIES);
        // Oldest (ts=0) should have been evicted; newest entry is at the end.
        assert_eq!(log.entries()[0].timestamp_ns, 1);
        assert_eq!(
            log.entries()[MAX_AUDIT_ENTRIES - 1].timestamp_ns,
            MAX_AUDIT_ENTRIES as u64
        );
    }

    #[test]
    fn audit_log_eviction_preserves_fifo_order() {
        // Push well past the cap and confirm the retained window is the most
        // recent contiguous run in oldest-to-newest order — i.e. front-pop
        // eviction (VecDeque) keeps FIFO semantics, not just O(1).
        let mut log = AuditLog::new();
        let total = MAX_AUDIT_ENTRIES + 500;
        for i in 0..total {
            log.push(AuditEntry {
                timestamp_ns: i as u64,
                signal_type: "view".to_string(),
                accepted: true,
                reason: None,
            });
        }
        assert!(log.truncated);
        assert_eq!(log.len(), MAX_AUDIT_ENTRIES);

        let entries = log.entries();
        // The oldest retained entry is total - MAX_AUDIT_ENTRIES; the rest follow
        // contiguously in ascending timestamp order.
        let first_retained = (total - MAX_AUDIT_ENTRIES) as u64;
        assert_eq!(entries[0].timestamp_ns, first_retained);
        assert_eq!(
            entries[MAX_AUDIT_ENTRIES - 1].timestamp_ns,
            (total - 1) as u64
        );
        for (offset, e) in entries.iter().enumerate() {
            assert_eq!(e.timestamp_ns, first_retained + offset as u64);
        }
    }
}
