use std::sync::Arc;

use axum::{
    extract::{Request, State},
    http::StatusCode,
    middleware::Next,
    response::{IntoResponse, Response},
};
use rustelo_core_lib::auth::Role;
use tokio::sync::RwLock;

use super::{
    effect_executor::{execute_effect, EffectContext},
    file_config::{FileRbacConfig, PolicyEffect, RbacOutcome, ResourceKind},
};
use crate::auth::middleware::AuthContext;

/// Map a `Role` variant to the lowercase group name used in `rbac.ncl`.
fn role_to_group(role: &Role) -> String {
    match role {
        Role::Admin => "admin".to_string(),
        Role::Moderator => "moderator".to_string(),
        Role::User => "user".to_string(),
        Role::Guest => "guest".to_string(),
        Role::Custom(s) => s.clone(),
    }
}

/// Compact label for an effect, used in log output.
fn effect_label(effect: &PolicyEffect) -> &'static str {
    match effect {
        PolicyEffect::Redirect { .. } => "redirect",
        PolicyEffect::Log { .. } => "log",
        PolicyEffect::AuditLog { .. } => "audit_log",
        PolicyEffect::Notify { .. } => "notify",
        PolicyEffect::Response { .. } => "response",
    }
}

/// Comma-separated effect labels for a slice of effects.
fn effects_summary(effects: &[PolicyEffect]) -> String {
    if effects.is_empty() {
        return String::new();
    }
    effects
        .iter()
        .map(effect_label)
        .collect::<Vec<_>>()
        .join(", ")
}

/// Infer `ResourceKind` from the request path using well-known path prefixes.
///
/// - `/api/` prefix → `Endpoint`
/// - Static asset prefixes (`/pkg/`, `/public/`, `/styles/`, `/js/`, `/css/`,
///   `/images/`, `/logos/`, `/assets/`) → `Asset`
/// - Everything else → `Page`
fn infer_resource_kind(path: &str) -> ResourceKind {
    if path.starts_with("/api/") || path == "/api" {
        return ResourceKind::Endpoint;
    }
    const ASSET_PREFIXES: &[&str] = &[
        "/pkg/", "/public/", "/styles/", "/js/", "/css/", "/images/", "/logos/", "/assets/",
    ];
    if ASSET_PREFIXES.iter().any(|p| path.starts_with(p)) {
        return ResourceKind::Asset;
    }
    ResourceKind::Page
}

/// File-based RBAC middleware.
///
/// Reads the shared `FileRbacConfig` under a read-lock (non-blocking) and
/// evaluates the policy for the current request. Effects are executed in
/// declaration order. The first terminant effect (`Redirect` or `Response`)
/// short-circuits the chain. If no terminant effect fires:
/// - `Allow` → passes the request to the next handler.
/// - `Deny`  → returns `403 Forbidden`.
pub async fn file_rbac_middleware(
    State(file_config): State<Arc<RwLock<FileRbacConfig>>>,
    request: Request,
    next: Next,
) -> Response {
    let path = request.uri().path().to_string();
    let method = request.method().as_str().to_string();

    // Extract AuthContext injected by auth_middleware (may be absent when
    // the auth feature is disabled or the request is unauthenticated).
    let auth_ctx = request.extensions().get::<AuthContext>().cloned();

    let (user_groups_owned, user_id_owned, user_email_owned): (
        Option<Vec<String>>,
        Option<String>,
        Option<String>,
    ) = match &auth_ctx {
        Some(ctx) if ctx.user.is_some() => {
            let user = ctx.user.as_ref().unwrap();
            let groups: Vec<String> = user.roles.iter().map(role_to_group).collect();
            // Use email as the user identity key (matches user_overrides[].id in rbac.ncl).
            let id = Some(user.email.clone());
            let email = Some(user.email.clone());
            (Some(groups), id, email)
        }
        _ => (None, None, None),
    };

    let user_groups_slice: Option<&[String]> = user_groups_owned.as_deref();
    let user_id: Option<&str> = user_id_owned.as_deref();
    let user_email: Option<&str> = user_email_owned.as_deref();

    let kind = infer_resource_kind(&path);
    let kind_str = match kind {
        ResourceKind::Endpoint => "endpoint",
        ResourceKind::Page => "page",
        ResourceKind::Asset => "asset",
    };

    let decision = {
        let cfg = file_config.read().await;
        cfg.evaluate(user_groups_slice, user_id, kind, &path, &method)
    };

    let user_label = user_id.unwrap_or("anonymous");
    let groups_label = user_groups_owned
        .as_deref()
        .map(|g| g.join(","))
        .unwrap_or_default();

    match decision.outcome {
        RbacOutcome::Deny => {
            tracing::info!(
                method = %method,
                path = %path,
                resource = kind_str,
                user = user_label,
                groups = %groups_label,
                effects = %effects_summary(&decision.effects),
                "rbac deny"
            );
        }
        RbacOutcome::Allow if !decision.effects.is_empty() => {
            tracing::debug!(
                method = %method,
                path = %path,
                resource = kind_str,
                user = user_label,
                effects = %effects_summary(&decision.effects),
                "rbac allow with effects"
            );
        }
        _ => {}
    }

    let effect_ctx = EffectContext {
        path: &path,
        method: &method,
        user_id,
        user_email,
    };

    // Execute effects in order; stop at the first terminant.
    for effect in &decision.effects {
        tracing::debug!(
            effect = effect_label(effect),
            path = %path,
            user = user_label,
            "rbac executing effect"
        );
        if let Some(response) = execute_effect(effect, &effect_ctx).await {
            return response;
        }
    }

    match decision.outcome {
        RbacOutcome::Allow => next.run(request).await,
        RbacOutcome::Deny => StatusCode::FORBIDDEN.into_response(),
    }
}
