//! Declarative macros for common authentication flows.
//!
//! These macros are thin wrappers around rok-auth primitives, designed to
//! reduce boilerplate in async Axum handler bodies.  They expand to plain Rust
//! expressions — no magic, fully traceable in compiler output.
//!
//! # Available macros
//!
//! | Macro | Returns |
//! |---|---|
//! | [`login!`] | `Result<TokenPair, AuthError>` (must `.await`) |
//! | [`register!`] | `Result<(), AuthError>` (must `.await`) |
//! | [`require_role!`] | early-returns `Err(AuthError::Forbidden)` |
//! | [`require_any_role!`] | early-returns `Err(AuthError::Forbidden)` |
//! | [`refresh_tokens!`] | `Result<TokenPair, AuthError>` |

// ── helpers called by macros ──────────────────────────────────────────────────

/// Called by the `login!` macro.  Not part of the public API.
#[doc(hidden)]
pub async fn __login_impl<U>(
    auth: &crate::Auth,
    pool: &sqlx::PgPool,
    email: &str,
    password: &str,
) -> Result<crate::TokenPair, crate::AuthError>
where
    U: crate::UserProvider,
{
    let user = U::find_by_email(pool, email)
        .await?
        .ok_or(crate::AuthError::InvalidCredentials)?;

    let ok = crate::password::verify_async(password.to_string(), user.password_hash().to_string())
        .await?;

    if !ok {
        return Err(crate::AuthError::InvalidCredentials);
    }

    let id_str = user.user_id().to_string();
    let access = auth.sign(&crate::Claims::new(&id_str, user.roles()))?;
    let refresh = auth.sign_refresh(&id_str)?;

    Ok(crate::TokenPair {
        access_token: access,
        refresh_token: refresh,
    })
}

/// Called by the `register!` macro.  Not part of the public API.
#[doc(hidden)]
pub async fn __register_impl<F, Fut>(
    email: &str,
    password: &str,
    persist: F,
) -> Result<(), crate::AuthError>
where
    F: FnOnce(String, String) -> Fut,
    Fut: std::future::Future<Output = Result<(), crate::AuthError>>,
{
    let hash = crate::password::hash_async(password.to_string()).await?;
    persist(email.to_string(), hash).await
}

// ── macros ────────────────────────────────────────────────────────────────────

/// Authenticate a user by email + password and return a [`crate::TokenPair`].
///
/// The type parameter `$U` must implement [`crate::UserProvider`].
///
/// Expands to a `Future` — use with `.await` or `?` after `.await`.
///
/// # Arguments
/// - `$auth`  — `&Auth`
/// - `$pool`  — `&sqlx::PgPool`
/// - `$email` — `&str`
/// - `$pw`    — `&str`
/// - `$U`     — a type that implements `UserProvider`
///
/// # Example
/// ```rust,ignore
/// let tokens = login!(auth, &pool, &body.email, &body.password, User)?;
/// ```
#[macro_export]
macro_rules! login {
    ($auth:expr, $pool:expr, $email:expr, $pw:expr, $U:ty) => {
        $crate::macros::__login_impl::<$U>($auth, $pool, $email, $pw).await
    };
}

/// Hash a password and persist a new user via the provided async closure.
///
/// # Arguments
/// - `$email`   — `&str`
/// - `$pw`      — `&str`
/// - `$persist` — `async |email: String, hash: String| -> Result<(), AuthError>`
///
/// # Example
/// ```rust,ignore
/// register!(&body.email, &body.password, |email, hash| async move {
///     User::create(&pool, &[("email", email.into()), ("password_hash", hash.into())])
///         .await
///         .map(|_| ())
///         .map_err(|e| AuthError::Internal(e.to_string()))
/// })?;
/// ```
#[macro_export]
macro_rules! register {
    ($email:expr, $pw:expr, $persist:expr) => {
        $crate::macros::__register_impl($email, $pw, $persist).await
    };
}

/// Early-return `Err(AuthError::Forbidden)` if `$claims` does not contain
/// `$role`.  Works in handlers returning `Result<_, AuthError>`.
///
/// # Example
/// ```rust,ignore
/// require_role!(claims, "admin");
/// ```
#[macro_export]
macro_rules! require_role {
    ($claims:expr, $role:expr) => {
        if !($claims).has_role($role) {
            return Err($crate::AuthError::Forbidden($role.to_string()));
        }
    };
}

/// Early-return `Err(AuthError::Forbidden)` if `$claims` does not contain at
/// least one of the listed roles.
///
/// # Example
/// ```rust,ignore
/// require_any_role!(claims, ["admin", "moderator"]);
/// ```
#[macro_export]
macro_rules! require_any_role {
    ($claims:expr, [$($role:expr),+ $(,)?]) => {
        if !($claims).has_any_role(&[$($role),+]) {
            return Err($crate::AuthError::Forbidden(
                [$($role),+].join(" | ")
            ));
        }
    };
}

/// Exchange a refresh token for a fresh [`crate::TokenPair`].
///
/// # Example
/// ```rust,ignore
/// let tokens = refresh_tokens!(auth, &body.refresh_token)?;
/// ```
#[macro_export]
macro_rules! refresh_tokens {
    ($auth:expr, $token:expr) => {
        ($auth)
            .exchange($token)
            .map(|(access_token, refresh_token)| $crate::TokenPair {
                access_token,
                refresh_token,
            })
    };
}
