//! Session management — sessions are Engram graph nodes connected to User nodes.
//!
//! Session lifecycle:
//! 1. `SessionManager::create()` → Session node + `User ──has_session──▶ Session` edge
//! 2. `SessionManager::validate()` → find Session node, check expiry
//! 3. `SessionManager::invalidate()` → delete Session node (edges auto-removed)
//!
//! The `SessionManager` works exclusively through the `EngramClient` trait — no
//! in-memory map, no cache. The graph is the source of truth.

use crate::{
    engram::EngramClient,
    error::{IdentityError, IdentityResult},
    nodes::{Session, EDGE_HAS_SESSION, NODE_SESSION},
};
use std::sync::Arc;

/// Manages session nodes in the Engram identity graph.
pub struct SessionManager {
    client: Arc<dyn EngramClient>,
    /// Default session TTL in seconds (default: 3600 = 1 hour).
    pub default_ttl_seconds: i64,
}

impl SessionManager {
    pub fn new(client: Arc<dyn EngramClient>) -> Self {
        Self {
            client,
            default_ttl_seconds: 3600,
        }
    }

    pub fn with_ttl(mut self, seconds: i64) -> Self {
        self.default_ttl_seconds = seconds;
        self
    }

    /// Create a new session for the given user.
    ///
    /// Stores the Session node in Engram and creates a `has_session` edge
    /// from the User node to the Session node.
    ///
    /// Returns the session ID string.
    pub fn create(
        &self,
        user_id: uuid::Uuid,
        ip_address: Option<String>,
    ) -> IdentityResult<Session> {
        let session = Session::new(user_id, self.default_ttl_seconds, ip_address);
        let session_id_str = session.id.to_string();

        // Store session node
        self.client
            .create_node(NODE_SESSION, session.to_value())
            .map_err(|e| IdentityError::GraphError(e.to_string()))?;

        // Edge: User → Session (has_session)
        self.client
            .create_edge(&user_id.to_string(), &session_id_str, EDGE_HAS_SESSION)
            .map_err(|e| IdentityError::GraphError(e.to_string()))?;

        Ok(session)
    }

    /// Validate a session by ID.
    ///
    /// Returns the `Session` node if found and not expired.
    /// Automatically deletes expired sessions on lookup (lazy expiry).
    pub fn validate(&self, session_id: &str) -> IdentityResult<Session> {
        let node = self
            .client
            .get_node(session_id)
            .map_err(|e| IdentityError::GraphError(e.to_string()))?
            .ok_or(IdentityError::SessionNotFound)?;

        let session = Session::from_value(&node)
            .ok_or_else(|| IdentityError::GraphError("session node parse failed".into()))?;

        if session.is_expired() {
            // Lazy cleanup — remove expired session from graph
            let _ = self.client.delete_node(session_id);
            return Err(IdentityError::SessionExpired);
        }

        Ok(session)
    }

    /// Invalidate (delete) a session node from the graph.
    pub fn invalidate(&self, session_id: &str) -> IdentityResult<()> {
        self.client
            .delete_node(session_id)
            .map_err(|e| IdentityError::GraphError(e.to_string()))
    }

    /// List all sessions for a user by traversing `has_session` edges.
    pub fn list_for_user(&self, user_id: &str) -> IdentityResult<Vec<Session>> {
        let nodes = self
            .client
            .find_connected(user_id, EDGE_HAS_SESSION)
            .map_err(|e| IdentityError::GraphError(e.to_string()))?;

        let sessions: Vec<Session> = nodes
            .iter()
            .filter_map(Session::from_value)
            .filter(|s| !s.is_expired())
            .collect();

        Ok(sessions)
    }

    /// Invalidate all sessions for a user (logout everywhere).
    pub fn invalidate_all_for_user(&self, user_id: &str) -> IdentityResult<usize> {
        let sessions = self.list_for_user(user_id)?;
        let count = sessions.len();
        for session in &sessions {
            let _ = self.client.delete_node(&session.id.to_string());
        }
        Ok(count)
    }
}
