//! Session snapshots: full state dumps and the `SessionContext` derived from them.

use std::collections::{HashMap, HashSet};

use crate::schema::Window;

use super::audit::AuditEntry;
use super::signal_state::SignalSnapEntry;
use super::state::SessionState;
use super::types::SessionId;

// ── SessionSnapshot ───────────────────────────────────────────────────────────

/// Full state dump of a session (active or archived).
///
/// Active sessions: scores are decayed to the current wall-clock time.
/// Archived sessions: scores are frozen at the moment of `close_session`.
#[derive(Debug, Clone)]
pub struct SessionSnapshot {
    pub id: SessionId,
    pub user_id: u64,
    pub signals_written: u64,
    pub signals_rejected: u64,
    pub duration_ms: u64,
    pub metadata: HashMap<String, String>,
    /// Timestamped annotations: `(timestamp_ns, hint_text)`.
    pub annotations: Vec<(u64, String)>,
    /// Frozen score of the "reward" signal at session close (or current score if active).
    pub reward_velocity: f64,
    /// Entity IDs that received session signals.
    pub signaled_entities: Vec<u64>,
    /// Policy audit log (populated on `close_session`; empty for active-session snapshots).
    ///
    /// For active sessions, use `TidalDb::session_audit()` to retrieve the live log.
    pub audit_log: Vec<AuditEntry>,
    /// `true` if the audit log was truncated due to the `MAX_AUDIT_ENTRIES` cap.
    pub audit_truncated: bool,
    /// Per-signal-type statistics (decay score + 1-hour window count + total count).
    pub signals: HashMap<String, SignalSnapEntry>,
    /// Session start time as nanoseconds since Unix epoch.
    pub started_at_ns: u64,
    /// Session close time as nanoseconds since Unix epoch.
    /// `0` for active-session snapshots (the session has not been closed yet).
    pub closed_at_ns: u64,
}

// ── SessionContext ────────────────────────────────────────────────────────────

/// Session context for FOR SESSION ranking boost.
///
/// Extracted from a `SessionSnapshot` by the query executor and passed to
/// `ProfileExecutor::score_with_session` to apply session-aware boosts.
#[derive(Debug, Clone)]
pub struct SessionContext {
    /// Keywords extracted from annotations (whitespace-split, lowercased).
    pub keywords: Vec<String>,
    /// Velocity/score of the "reward" signal (for velocity-based boost).
    pub reward_velocity: f64,
    /// Session metadata.
    pub metadata: HashMap<String, String>,
    /// Entity IDs that received session signals (for entity-level boost).
    pub signaled_entities: HashSet<u64>,
}

impl SessionContext {
    /// Build a `SessionContext` from a `SessionSnapshot`.
    ///
    /// Keywords are extracted from annotations by splitting on whitespace and
    /// lowercasing. Duplicates are removed via a `HashSet` before collecting.
    #[must_use]
    pub fn from_snapshot(snapshot: &SessionSnapshot) -> Self {
        let keywords: Vec<String> = snapshot
            .annotations
            .iter()
            .flat_map(|(_ts, hint)| {
                hint.split_whitespace()
                    .map(str::to_lowercase)
                    .collect::<Vec<_>>()
            })
            .collect::<HashSet<_>>()
            .into_iter()
            .collect();

        let signaled_entities: HashSet<u64> = snapshot.signaled_entities.iter().copied().collect();

        Self {
            keywords,
            reward_velocity: snapshot.reward_velocity,
            metadata: snapshot.metadata.clone(),
            signaled_entities,
        }
    }
}

// ── Snapshot building ─────────────────────────────────────────────────────────

/// Build a live `SessionSnapshot` from an active `SessionState`.
///
/// Scores are decayed to `now_ns`. The `audit_log` field is left empty — for
/// active sessions the live audit log is accessible via `TidalDb::session_audit()`.
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn build_snapshot(state: &SessionState, now_ns: u64) -> SessionSnapshot {
    let reward_velocity = state
        .signals
        .get("reward")
        .map_or(0.0, |ss| ss.current_score(now_ns));

    let annotations = state
        .annotations
        .lock()
        .map(|guard| guard.clone())
        .unwrap_or_default();

    let signaled_entities: Vec<u64> = state.signaled_entities.iter().map(|e| *e.key()).collect();

    let duration_ms = state.started_at.elapsed().as_millis() as u64;

    // Build per-signal stats: decay score decayed to now_ns.
    let signals: HashMap<String, SignalSnapEntry> = state
        .signals
        .iter()
        .map(|entry| {
            let name = entry.key().clone();
            let ss = entry.value();
            (
                name,
                SignalSnapEntry {
                    decay_score: ss.current_score(now_ns),
                    window_1h: ss.windows.windowed_count(Window::OneHour),
                    count: ss.hot.count(),
                },
            )
        })
        .collect();

    let (audit_log_live, audit_truncated_live) = state
        .audit_log
        .lock()
        .map(|g| (Vec::new(), g.truncated))
        .unwrap_or_default();

    SessionSnapshot {
        id: state.id,
        user_id: state.user_id,
        signals_written: state
            .signals_written
            .load(std::sync::atomic::Ordering::Relaxed),
        signals_rejected: state
            .signals_rejected
            .load(std::sync::atomic::Ordering::Relaxed),
        duration_ms,
        metadata: state.metadata.clone(),
        annotations,
        reward_velocity,
        signaled_entities,
        audit_log: audit_log_live,
        audit_truncated: audit_truncated_live,
        signals,
        started_at_ns: state.started_at_ns,
        closed_at_ns: 0,
    }
}

/// Build a frozen `SessionSnapshot` at close time (no further decay).
///
/// Captures the full audit log (with truncation flag) so it remains accessible
/// after the session is removed from active state.
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn build_frozen_snapshot(state: &SessionState, duration_ms: u64) -> SessionSnapshot {
    let reward_velocity = state
        .signals
        .get("reward")
        .map_or(0.0, |ss| ss.frozen_score());

    let annotations = state
        .annotations
        .lock()
        .map(|guard| guard.clone())
        .unwrap_or_default();

    let (audit_entries, audit_truncated) = state
        .audit_log
        .lock()
        .map(|guard| (guard.entries().to_vec(), guard.truncated))
        .unwrap_or_default();

    let signaled_entities: Vec<u64> = state.signaled_entities.iter().map(|e| *e.key()).collect();

    // Build per-signal stats: frozen scores (no further decay).
    let signals: HashMap<String, SignalSnapEntry> = state
        .signals
        .iter()
        .map(|entry| {
            let name = entry.key().clone();
            let ss = entry.value();
            (
                name,
                SignalSnapEntry {
                    decay_score: ss.frozen_score(),
                    window_1h: ss.windows.windowed_count(Window::OneHour),
                    count: ss.hot.count(),
                },
            )
        })
        .collect();

    SessionSnapshot {
        id: state.id,
        user_id: state.user_id,
        signals_written: state
            .signals_written
            .load(std::sync::atomic::Ordering::Relaxed),
        signals_rejected: state
            .signals_rejected
            .load(std::sync::atomic::Ordering::Relaxed),
        duration_ms,
        metadata: state.metadata.clone(),
        annotations,
        reward_velocity,
        signaled_entities,
        audit_log: audit_entries,
        audit_truncated,
        signals,
        started_at_ns: state.started_at_ns,
        closed_at_ns: crate::schema::Timestamp::now().as_nanos(),
    }
}

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

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

    use super::super::types::SessionId;

    #[test]
    fn session_context_keywords_extracted() {
        let snap = SessionSnapshot {
            id: SessionId(1),
            user_id: 1,
            signals_written: 0,
            signals_rejected: 0,
            duration_ms: 0,
            metadata: HashMap::new(),
            annotations: vec![
                (0u64, "rust programming".to_string()),
                (1u64, "systems databases".to_string()),
            ],
            reward_velocity: 0.5,
            signaled_entities: vec![10, 20],
            audit_log: Vec::new(),
            audit_truncated: false,
            signals: HashMap::new(),
            started_at_ns: 0,
            closed_at_ns: 0,
        };
        let ctx = SessionContext::from_snapshot(&snap);
        assert!(ctx.keywords.contains(&"rust".to_string()));
        assert!(ctx.keywords.contains(&"programming".to_string()));
        assert!(ctx.keywords.contains(&"systems".to_string()));
        assert!(ctx.keywords.contains(&"databases".to_string()));
        assert_eq!(ctx.reward_velocity, 0.5);
        assert!(ctx.signaled_entities.contains(&10));
        assert!(ctx.signaled_entities.contains(&20));
    }
}
