//! Live runtime state structs for active sessions.

use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::{Arc, Mutex};
use std::time::Instant;

use dashmap::DashMap;

use crate::wal::format::session::SessionSeqNo;

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

// ── SessionState ──────────────────────────────────────────────────────────────

/// Runtime state for an active session. Stored in `TidalDb::sessions`.
pub struct SessionState {
    pub id: SessionId,
    pub user_id: u64,
    pub agent_id: AgentId,
    pub policy_name: String,
    /// Wall-clock instant when the session was started.
    pub started_at: Instant,
    /// `started_at` expressed as nanoseconds since Unix epoch (for archival).
    pub started_at_ns: u64,
    /// Caller-supplied metadata attached to the session.
    pub metadata: HashMap<String, String>,
    /// Per-signal-type combined decay + window state (keyed by signal type name).
    pub signals: DashMap<String, SessionSignalState>,
    /// Entity IDs that have received any signal in this session.
    pub signaled_entities: DashMap<u64, ()>,
    /// Timestamped free-text annotations (e.g., preference hints).
    /// Each entry is `(timestamp_ns, hint_text)`. Capped at 100 entries.
    pub annotations: Mutex<Vec<(u64, String)>>,
    /// Total signals accepted (for audit and policy count cap).
    pub signals_written: AtomicU64,
    /// Total signals rejected by policy.
    pub signals_rejected: AtomicU64,
    /// Policy audit log with bounded eviction.
    pub audit_log: Mutex<AuditLog>,
    /// `true` once `close_session` has consumed the handle.
    pub closed: Arc<AtomicBool>,
}

// ── SessionHandle ─────────────────────────────────────────────────────────────

/// Move-only handle to an active session.
///
/// Ownership is consumed by `TidalDb::close_session` to prevent use-after-close
/// at the type level. The `closed` `Arc<AtomicBool>` provides runtime
/// defense-in-depth for any clone held separately.
#[derive(Debug)]
pub struct SessionHandle {
    pub id: SessionId,
    pub user_id: u64,
    pub agent_id: AgentId,
    pub policy_name: String,
    pub started_at: Instant,
    /// Shared with `SessionState`; set to `true` by `close_session`.
    pub closed: Arc<AtomicBool>,
}

// ── SessionSeqNoTracker ──────────────────────────────────────────────────────

/// Per-session monotonic write counter for idempotent replay.
///
/// Tracks the highest seqno applied locally. Writes with seqno <= hwm
/// are silently dropped (idempotent replay; state is already reflected).
#[derive(Debug, Default)]
pub struct SessionSeqNoTracker {
    hwm: DashMap<u64, SessionSeqNo>, // keyed by session_id.as_u64()
}

impl SessionSeqNoTracker {
    #[must_use]
    pub fn new() -> Self {
        Self {
            hwm: DashMap::new(),
        }
    }

    /// Returns `true` if this write should be applied (seqno > hwm).
    /// Returns `false` if the write is a duplicate -- skip.
    /// Updates the HWM on accept.
    #[must_use]
    pub fn should_apply(&self, session_id: u64, seqno: SessionSeqNo) -> bool {
        let mut entry = self.hwm.entry(session_id).or_insert(SessionSeqNo::ZERO);
        if seqno > *entry {
            *entry = seqno;
            true
        } else {
            false
        }
    }

    /// Current HWM for a session (ZERO if unknown).
    #[must_use]
    pub fn hwm(&self, session_id: u64) -> SessionSeqNo {
        self.hwm.get(&session_id).map_or(SessionSeqNo::ZERO, |v| *v)
    }

    /// Set HWM directly (used on follower startup to restore from WAL).
    pub fn set_hwm(&self, session_id: u64, seqno: SessionSeqNo) {
        self.hwm.insert(session_id, seqno);
    }
}

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

    #[test]
    fn seqno_tracker_accepts_monotone() {
        let t = SessionSeqNoTracker::new();
        assert!(t.should_apply(1, SessionSeqNo(1)));
        assert!(t.should_apply(1, SessionSeqNo(2)));
        assert!(t.should_apply(1, SessionSeqNo(5)));
        assert_eq!(t.hwm(1), SessionSeqNo(5));
    }

    #[test]
    fn seqno_tracker_rejects_duplicate() {
        let t = SessionSeqNoTracker::new();
        assert!(t.should_apply(1, SessionSeqNo(3)));
        assert!(!t.should_apply(1, SessionSeqNo(3))); // duplicate
        assert!(!t.should_apply(1, SessionSeqNo(2))); // older
        assert!(t.should_apply(1, SessionSeqNo(4))); // resumes
    }

    #[test]
    fn seqno_tracker_independent_sessions() {
        let t = SessionSeqNoTracker::new();
        assert!(t.should_apply(1, SessionSeqNo(10)));
        assert!(t.should_apply(2, SessionSeqNo(1))); // separate session starts fresh
        assert_eq!(t.hwm(1), SessionSeqNo(10));
        assert_eq!(t.hwm(2), SessionSeqNo(1));
    }

    #[test]
    fn seqno_tracker_legacy_none_seqno_always_applies() {
        // Legacy events have no seqno; the caller skips the should_apply check
        // for None. Verify tracker treats unknown session as HWM=ZERO.
        let t = SessionSeqNoTracker::new();
        assert_eq!(t.hwm(999), SessionSeqNo::ZERO);
    }
}
