//! Role-based access guard extractor.
//!
//! [`RequireRole`] is a zero-sized extractor that enforces role membership
//! before the handler body executes.
//!
//! Because Rust stable does not yet support `&'static str` const generics,
//! the role is encoded via the [`RoleMarker`] trait.  Define a unit struct
//! for each role you need and implement `RoleMarker` for it:
//!
//! ```rust,no_run
//! use rok_auth::axum::{RoleMarker, RequireRole};
//! use rok_auth::Claims;
//! use axum::{routing::get, Router};
//!
//! pub struct Admin;
//! impl RoleMarker for Admin { const ROLE: &'static str = "admin"; }
//!
//! async fn admin_panel(
//!     _: RequireRole<Admin>,
//!     claims: Claims,
//! ) -> String {
//!     format!("welcome admin {}", claims.sub)
//! }
//!
//! let app: Router = Router::new().route("/admin", get(admin_panel));
//! ```

use std::marker::PhantomData;

use axum::{extract::FromRequestParts, http::request::Parts, response::Response};

use super::extractor::OptionalClaims;
use crate::AuthError;

/// Implement this for a unit struct to create a typed role guard.
///
/// ```rust
/// use rok_auth::axum::RoleMarker;
///
/// pub struct Admin;
/// impl RoleMarker for Admin { const ROLE: &'static str = "admin"; }
/// ```
pub trait RoleMarker: Send + Sync + 'static {
    const ROLE: &'static str;
}

/// Zero-sized extractor that resolves only when the request carries a valid
/// JWT with the role defined by `R: RoleMarker`.
///
/// Returns `401 Unauthorized` if no valid token is present, or
/// `403 Forbidden` if the role is missing.
pub struct RequireRole<R: RoleMarker>(PhantomData<R>);

impl<S, R: RoleMarker> FromRequestParts<S> for RequireRole<R>
where
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        use axum::response::IntoResponse;

        let OptionalClaims(maybe) = OptionalClaims::from_request_parts(parts, state)
            .await
            .unwrap();

        let claims = maybe.ok_or_else(|| AuthError::InvalidToken.into_response())?;

        if !claims.has_role(R::ROLE) {
            return Err(AuthError::Forbidden(R::ROLE.to_string()).into_response());
        }

        Ok(RequireRole(PhantomData))
    }
}
