//! EngramSessionStore — a `SessionProvider`-compatible store backed by the Engram graph.
//!
//! Sessions are Engram graph nodes (via el-identity's `SessionManager`).
//! This allows server-side invalidation even for stateless JWT workflows:
//! on every request, the JWT's `session_id` claim is used to look up the
//! Session node in Engram. If the node is missing or expired, the request
//! is rejected regardless of JWT validity.
//!
//! Dependency: takes `Arc<dyn EngramClient>` — soft dependency, no tight coupling
//! to the Engram crate.

use crate::{AuthContext, AuthError, AuthProvider, AuthResult, AuthUser, RoleRegistry};
use el_identity::{
    engram::EngramClient,
    session::SessionManager,
};
use std::sync::Arc;

/// Session provider backed by the Engram identity graph.
///
/// Sessions issued by this provider are stored as Engram Session nodes.
/// The session ID returned is the Engram node's UUID, which is also embedded
/// in JWTs via `JwtClaims::session_id`.
pub struct EngramSessionStore {
    session_manager: Arc<SessionManager>,
    client: Arc<dyn EngramClient>,
}

impl EngramSessionStore {
    /// Create a new `EngramSessionStore`.
    ///
    /// `client` is the Engram graph client. In production, pass your real
    /// Engram client. In tests, use `el_identity::engram::MockEngramClient`.
    pub fn new(client: Arc<dyn EngramClient>) -> Self {
        let sm = Arc::new(SessionManager::new(client.clone()));
        Self { session_manager: sm, client }
    }

    /// Create with a custom session TTL (seconds).
    pub fn with_ttl(client: Arc<dyn EngramClient>, ttl_seconds: i64) -> Self {
        let sm = Arc::new(SessionManager::new(client.clone()).with_ttl(ttl_seconds));
        Self { session_manager: sm, client }
    }

    /// Expose the underlying SessionManager for advanced use (e.g., listing sessions).
    pub fn session_manager(&self) -> &Arc<SessionManager> {
        &self.session_manager
    }
}

impl AuthProvider for EngramSessionStore {
    fn name(&self) -> &'static str {
        "engram_session"
    }

    /// Verify a session by its ID (Engram Session node UUID).
    ///
    /// Validates expiry via graph lookup. Returns `AuthContext` populated from
    /// the Session and User nodes.
    fn verify(&self, session_id: &str) -> AuthResult<AuthContext> {
        // Validate session node (checks expiry, lazy-deletes expired)
        let session = self
            .session_manager
            .validate(session_id)
            .map_err(|e| match e {
                el_identity::IdentityError::SessionNotFound => AuthError::SessionNotFound,
                el_identity::IdentityError::SessionExpired => AuthError::SessionNotFound,
                other => AuthError::Config(other.to_string()),
            })?;

        // Load user node
        let user_id_str = session.user_id.to_string();
        let user_node = self
            .client
            .get_node(&user_id_str)
            .map_err(|e| AuthError::Config(e.to_string()))?
            .ok_or(AuthError::InvalidCredentials)?;

        let identity_user = el_identity::User::from_value(&user_node)
            .ok_or_else(|| AuthError::Config("user node parse failed".into()))?;

        let auth_user = AuthUser::new(
            identity_user.id.to_string(),
            &identity_user.email,
            &identity_user.display_name,
        );

        // Load roles via has_role edges
        let role_nodes = self
            .client
            .find_connected(&user_id_str, el_identity::nodes::EDGE_HAS_ROLE)
            .map_err(|e| AuthError::Config(e.to_string()))?;

        let role_names: Vec<String> = role_nodes
            .iter()
            .filter_map(el_identity::Role::from_value)
            .map(|r| r.name)
            .collect();

        Ok(AuthContext::authenticated(auth_user, role_names, session_id))
    }

    /// Issue a new Engram-backed session for the given user.
    ///
    /// The user must already exist as a User node in Engram. Returns the
    /// session ID (UUID string) which should be embedded in the JWT's
    /// `session_id` claim.
    fn issue(&self, user: AuthUser, _role_registry: &RoleRegistry) -> AuthResult<String> {
        // Parse user ID as UUID
        let user_uuid = uuid::Uuid::parse_str(&user.id)
            .map_err(|_| AuthError::Config(format!("invalid user ID UUID: {}", user.id)))?;

        let session = self
            .session_manager
            .create(user_uuid, None)
            .map_err(|e| AuthError::Config(e.to_string()))?;

        Ok(session.id.to_string())
    }

    /// Revoke a session by deleting the Session node from the graph.
    fn revoke(&self, session_id: &str) -> AuthResult<()> {
        self.session_manager
            .invalidate(session_id)
            .map_err(|e| AuthError::Config(e.to_string()))
    }
}

