//! AuthGuard — the mechanism behind `@authenticate`.
//!
//! `AuthGuard` is the bridge between a raw session/JWT token string (extracted
//! from the request) and a fully-resolved `IdentityContext`.
//!
//! Execution:
//! 1. Extract session ID from the token (JWT decode or opaque lookup)
//! 2. Validate the Session node in Engram (expiry check)
//! 3. Load the User node
//! 4. Traverse `User ──has_role──▶ Role` edges
//! 5. Traverse `Role ──grants──▶ Scope` edges
//! 6. Return `IdentityContext` — fully resolved, ready for downstream use
//!
//! `@public` bypasses this guard entirely (see `el-aop::PublicMarker`).

use crate::{
    context::IdentityContext,
    engram::EngramClient,
    error::{IdentityError, IdentityResult},
    nodes::{Role, Scope, User, EDGE_GRANTS, EDGE_HAS_ROLE, NODE_ROLE, NODE_SCOPE, NODE_USER},
    session::SessionManager,
};
use std::sync::Arc;

/// AuthGuard resolves a session/token string into a full `IdentityContext`.
///
/// Configured once at application startup and shared across requests.
pub struct AuthGuard {
    client: Arc<dyn EngramClient>,
    session_manager: Arc<SessionManager>,
}

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

    /// Authenticate a request given its session ID.
    ///
    /// This is called by `@authenticate` in the AOP chain. For every protected
    /// endpoint, this runs before the handler. If it returns `Err`, the request
    /// is rejected.
    pub fn authenticate(&self, session_id: &str) -> IdentityResult<IdentityContext> {
        // 1. Validate session (checks expiry, lazy-deletes expired)
        let session = self.session_manager.validate(session_id)?;

        // 2. 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| IdentityError::GraphError(e.to_string()))?
            .ok_or_else(|| IdentityError::UserNotFound(user_id_str.clone()))?;

        let user = User::from_value(&user_node)
            .ok_or_else(|| IdentityError::GraphError("user node parse failed".into()))?;

        // 3. Load roles via has_role edges
        let role_nodes = self
            .client
            .find_connected(&user_id_str, EDGE_HAS_ROLE)
            .map_err(|e| IdentityError::GraphError(e.to_string()))?;

        let roles: Vec<Role> = role_nodes
            .iter()
            .filter_map(Role::from_value)
            .collect();

        // 4. Load scopes via grants edges from each role
        let mut scopes: Vec<Scope> = Vec::new();
        for role in &roles {
            let scope_nodes = self
                .client
                .find_connected(&role.id.to_string(), EDGE_GRANTS)
                .map_err(|e| IdentityError::GraphError(e.to_string()))?;
            scopes.extend(scope_nodes.iter().filter_map(Scope::from_value));
        }

        // Deduplicate scopes by name
        scopes.dedup_by(|a, b| a.name == b.name);

        Ok(IdentityContext::new(user, session, roles, scopes))
    }

    /// Require a specific role — returns Err::Forbidden if missing.
    pub fn require_role(
        &self,
        ctx: &IdentityContext,
        role: &str,
    ) -> IdentityResult<()> {
        if ctx.has_role(role) {
            Ok(())
        } else {
            Err(IdentityError::Forbidden(role.to_string()))
        }
    }

    /// Require a specific scope — returns Err::ScopeForbidden if missing.
    pub fn require_scope(
        &self,
        ctx: &IdentityContext,
        scope: &str,
    ) -> IdentityResult<()> {
        if ctx.has_scope(scope) {
            Ok(())
        } else {
            Err(IdentityError::ScopeForbidden(scope.to_string()))
        }
    }

    /// Register a user in the Engram graph.
    ///
    /// Creates the User node. Call this after first OAuth login or on signup.
    pub fn register_user(&self, user: &User) -> IdentityResult<()> {
        self.client
            .create_node(NODE_USER, user.to_value())
            .map(|_| ())
            .map_err(|e| IdentityError::GraphError(e.to_string()))
    }

    /// Assign a role to a user by creating a `has_role` edge.
    pub fn assign_role(&self, user_id: &str, role: &Role) -> IdentityResult<()> {
        // Ensure role node exists
        if self.client.get_node(&role.id.to_string())
            .map_err(|e| IdentityError::GraphError(e.to_string()))?
            .is_none()
        {
            self.client
                .create_node(NODE_ROLE, role.to_value())
                .map_err(|e| IdentityError::GraphError(e.to_string()))?;
        }
        // Edge: User → Role
        self.client
            .create_edge(user_id, &role.id.to_string(), EDGE_HAS_ROLE)
            .map_err(|e| IdentityError::GraphError(e.to_string()))
    }

    /// Register a scope and link it to a role via a `grants` edge.
    pub fn assign_scope_to_role(&self, role: &Role, scope: &Scope) -> IdentityResult<()> {
        if self.client.get_node(&scope.id.to_string())
            .map_err(|e| IdentityError::GraphError(e.to_string()))?
            .is_none()
        {
            self.client
                .create_node(NODE_SCOPE, scope.to_value())
                .map_err(|e| IdentityError::GraphError(e.to_string()))?;
        }
        self.client
            .create_edge(&role.id.to_string(), &scope.id.to_string(), EDGE_GRANTS)
            .map_err(|e| IdentityError::GraphError(e.to_string()))
    }
}
