//! Axum extractors and middleware for JWT auth + RBAC.

use axum::{
    body::Body,
    extract::State,
    http::{Request, StatusCode, header::AUTHORIZATION},
    middleware::Next,
    response::{IntoResponse, Response},
};
use serde_json::json;

use crate::AppState;
use crate::auth::{Claims, RbacPolicy};

/// Axum middleware: validate Bearer JWT and attach `Claims` as an extension.
///
/// Routes that opt in via `.layer(from_fn_with_state(state, auth_layer))`
/// can then extract `Extension<Claims>` to access the caller identity and roles.
pub async fn auth_layer(
    State(app): State<AppState>,
    mut req: Request<Body>,
    next: Next,
) -> Response {
    let Some(ref jwt) = app.jwt else {
        // Solo mode: inject synthetic admin Claims so downstream handlers that
        // extract `Extension<Claims>` don't 500 on MissingExtension. Expiry is
        // u64::MAX so time-based validation (if ever re-checked) never trips.
        let claims = crate::auth::Claims {
            sub: "solo".into(),
            exp: u64::MAX,
            roles: vec!["admin".into()],
        };
        req.extensions_mut().insert(claims);
        return next.run(req).await;
    };

    let auth_header = req
        .headers()
        .get(AUTHORIZATION)
        .and_then(|v| v.to_str().ok());

    let Some(header_val) = auth_header else {
        return error_response(StatusCode::UNAUTHORIZED, "missing Authorization header");
    };

    let token = match crate::auth::JwtConfig::extract_bearer(header_val) {
        Ok(t) => t,
        Err(_) => return error_response(StatusCode::UNAUTHORIZED, "malformed Authorization header"),
    };

    let claims = match jwt.validate(token) {
        Ok(c) => c,
        Err(e) => return error_response(StatusCode::UNAUTHORIZED, &e.to_string()),
    };

    req.extensions_mut().insert(claims);
    next.run(req).await
}

/// Utility: check RBAC for a resolved tool category.
///
/// Returns `Ok(())` when the caller has sufficient privileges, or a
/// `403 Forbidden` response body when they do not.
pub fn check_rbac(
    claims: &Claims,
    category: provisioning_core::protocol::ToolCategory,
) -> Result<(), Box<Response>> {
    RbacPolicy::check(claims, category)
        .map_err(|e| Box::new(error_response(StatusCode::FORBIDDEN, &e.to_string())))
}

fn error_response(status: StatusCode, msg: &str) -> Response {
    (status, axum::Json(json!({"error": msg}))).into_response()
}
