//! In-memory domain state shared across the HTTP router and background workers.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{
    collections::{HashMap, VecDeque},
    sync::{Arc, RwLock},
    time::Instant,
};
use uuid::Uuid;

const MAX_RECENT: usize = 100;

// ── Records ───────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolRecord {
    pub tool: String,
    pub params: Value,
    pub outcome: InvocationOutcome,
    pub duration_ms: u64,
    pub invoked_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum InvocationOutcome {
    Ok { value: Value },
    Err { code: i32, message: String },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
    pub id: String,
    pub opened_at: DateTime<Utc>,
    pub tool_count: u32,
    /// Subject (`Claims.sub`) of the principal who opened this session.
    /// Used to prevent cross-session counter tampering.
    pub opened_by: String,
}

// ── DomainState ───────────────────────────────────────────────────────────────

struct Inner {
    recent: VecDeque<ToolRecord>,
    sessions: HashMap<String, SessionInfo>,
    last_config_reload: Option<Instant>,
    total_invocations: u64,
}

/// Cheap-clone shared state for provisioning-daemon — wrap in `Arc` once,
/// pass clones to axum `State` and background tasks.
#[derive(Clone)]
pub struct DomainState(Arc<RwLock<Inner>>);

impl DomainState {
    pub fn new() -> Self {
        Self(Arc::new(RwLock::new(Inner {
            recent: VecDeque::with_capacity(MAX_RECENT),
            sessions: HashMap::new(),
            last_config_reload: None,
            total_invocations: 0,
        })))
    }

    /// Record a completed tool invocation (success or failure).
    pub fn record_invocation(&self, record: ToolRecord) {
        let mut g = self.0.write().expect("domain state poisoned");
        g.total_invocations += 1;
        if g.recent.len() >= MAX_RECENT {
            g.recent.pop_front();
        }
        g.recent.push_back(record);
    }

    /// Return the `n` most recent invocations (newest last).
    pub fn recent(&self, n: usize) -> Vec<ToolRecord> {
        let g = self.0.read().expect("domain state poisoned");
        g.recent.iter().rev().take(n).cloned().collect::<Vec<_>>().into_iter().rev().collect()
    }

    pub fn open_session(&self, opened_by: impl Into<String>) -> String {
        let id = Uuid::new_v4().to_string();
        let mut g = self.0.write().expect("domain state poisoned");
        g.sessions.insert(
            id.clone(),
            SessionInfo {
                id: id.clone(),
                opened_at: Utc::now(),
                tool_count: 0,
                opened_by: opened_by.into(),
            },
        );
        id
    }

    pub fn close_session(&self, id: &str) {
        let mut g = self.0.write().expect("domain state poisoned");
        g.sessions.remove(id);
    }

    /// Increment the tool counter for `id` only when `principal` matches the
    /// session's opener. Returns `true` on a successful increment, `false`
    /// when the session is unknown or owned by a different principal
    /// (prevents cross-session counter tampering).
    pub fn increment_session_tool_count(&self, id: &str, principal: &str) -> bool {
        let mut g = self.0.write().expect("domain state poisoned");
        match g.sessions.get_mut(id) {
            Some(s) if s.opened_by == principal => {
                s.tool_count += 1;
                true
            }
            _ => false,
        }
    }

    pub fn sessions(&self) -> Vec<SessionInfo> {
        let g = self.0.read().expect("domain state poisoned");
        g.sessions.values().cloned().collect()
    }

    pub fn mark_config_reload(&self) {
        let mut g = self.0.write().expect("domain state poisoned");
        g.last_config_reload = Some(Instant::now());
    }

    /// Seconds since last config reload, or `None` if never reloaded.
    pub fn secs_since_reload(&self) -> Option<u64> {
        let g = self.0.read().expect("domain state poisoned");
        g.last_config_reload.map(|t| t.elapsed().as_secs())
    }

    pub fn total_invocations(&self) -> u64 {
        let g = self.0.read().expect("domain state poisoned");
        g.total_invocations
    }
}

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

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

    fn ok_record(tool: &str) -> ToolRecord {
        ToolRecord {
            tool: tool.to_owned(),
            params: json!({}),
            outcome: InvocationOutcome::Ok { value: json!({"ok": true}) },
            duration_ms: 10,
            invoked_at: Utc::now(),
        }
    }

    #[test]
    fn record_and_recent() {
        let state = DomainState::new();
        state.record_invocation(ok_record("tool_a"));
        state.record_invocation(ok_record("tool_b"));
        let recent = state.recent(10);
        assert_eq!(recent.len(), 2);
        assert_eq!(recent[0].tool, "tool_a");
        assert_eq!(recent[1].tool, "tool_b");
    }

    #[test]
    fn recent_capped_at_max() {
        let state = DomainState::new();
        for i in 0..=MAX_RECENT {
            state.record_invocation(ok_record(&format!("tool_{i}")));
        }
        assert_eq!(state.recent(MAX_RECENT + 10).len(), MAX_RECENT);
    }

    #[test]
    fn session_lifecycle() {
        let state = DomainState::new();
        let id = state.open_session("alice");
        assert_eq!(state.sessions().len(), 1);
        assert!(state.increment_session_tool_count(&id, "alice"));
        state.close_session(&id);
        assert!(state.sessions().is_empty());
    }

    #[test]
    fn cross_principal_increment_rejected() {
        let state = DomainState::new();
        let id = state.open_session("alice");
        assert!(!state.increment_session_tool_count(&id, "mallory"));
        assert!(!state.increment_session_tool_count("nonexistent", "alice"));
    }

    #[test]
    fn total_invocations_increments() {
        let state = DomainState::new();
        state.record_invocation(ok_record("t"));
        state.record_invocation(ok_record("t"));
        assert_eq!(state.total_invocations(), 2);
    }

    #[test]
    fn config_reload_marks_time() {
        let state = DomainState::new();
        assert!(state.secs_since_reload().is_none());
        state.mark_config_reload();
        assert!(state.secs_since_reload().is_some());
    }
}
