//! Auth extractors.
//!
//! Three extractors are available, in increasing strictness:
//!
//! - [`CurrentUser`] — accepts either a session cookie or an
//!   `Authorization: Bearer <token>` header. Used by ordinary
//!   authenticated endpoints where bot tokens are first-class clients.
//! - [`CurrentSessionUser`] — accepts only the session cookie. Used as
//!   the substrate for admin extraction so bot tokens cannot authenticate
//!   as the admin (see [`RequireAdmin`]).
//! - [`RequireAdmin`] — composes over [`CurrentSessionUser`] and
//!   additionally requires `user.is_admin`. Returns 403 for
//!   authenticated-but-not-admin, 401 otherwise.
//!
//! All lookups go by `sha256(raw_token)` — the raw value is never stored
//! in the database.

use axum::async_trait;
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum_extra::extract::cookie::CookieJar;
use axum_extra::headers::authorization::Bearer;
use axum_extra::headers::Authorization;
use axum_extra::TypedHeader;

use crate::app::AppState;
use crate::auth::token::hash_token;
use crate::domain::User;
use crate::error::AppError;
use crate::repo;

pub const SESSION_COOKIE_NAME: &str = "mangalord_session";

pub struct CurrentUser(pub User);

#[async_trait]
impl FromRequestParts<AppState> for CurrentUser {
    type Rejection = AppError;

    async fn from_request_parts(
        parts: &mut Parts,
        state: &AppState,
    ) -> Result<Self, Self::Rejection> {
        let jar = CookieJar::from_headers(&parts.headers);
        if let Some(cookie) = jar.get(SESSION_COOKIE_NAME) {
            let hash = hash_token(cookie.value());
            if let Some(session) = repo::session::find_active(&state.db, &hash).await? {
                if let Some(user) = repo::user::find_by_id(&state.db, session.user_id).await? {
                    return Ok(CurrentUser(user));
                }
            }
        }

        if let Ok(TypedHeader(Authorization(bearer))) =
            TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state).await
        {
            let hash = hash_token(bearer.token());
            if let Some(token) = repo::api_token::find_active(&state.db, &hash).await? {
                if let Some(user) = repo::user::find_by_id(&state.db, token.user_id).await? {
                    // Fire-and-forget would be ideal but the test harness needs
                    // a deterministic write so the touched timestamp shows up
                    // when the test inspects state. Synchronous is fine.
                    let _ = repo::api_token::touch_last_used(&state.db, token.id).await;
                    return Ok(CurrentUser(user));
                }
            }
        }

        Err(AppError::Unauthenticated)
    }
}

/// Cookie-only authentication. Bot/API tokens are explicitly NOT accepted
/// here — this is the substrate for [`RequireAdmin`] and exists precisely
/// to keep admin authority out of bearer-token reach.
pub struct CurrentSessionUser(pub User);

#[async_trait]
impl FromRequestParts<AppState> for CurrentSessionUser {
    type Rejection = AppError;

    async fn from_request_parts(
        parts: &mut Parts,
        state: &AppState,
    ) -> Result<Self, Self::Rejection> {
        let jar = CookieJar::from_headers(&parts.headers);
        let cookie = jar
            .get(SESSION_COOKIE_NAME)
            .ok_or(AppError::Unauthenticated)?;
        let hash = hash_token(cookie.value());
        let session = repo::session::find_active(&state.db, &hash)
            .await?
            .ok_or(AppError::Unauthenticated)?;
        let user = repo::user::find_by_id(&state.db, session.user_id)
            .await?
            .ok_or(AppError::Unauthenticated)?;
        Ok(CurrentSessionUser(user))
    }
}

/// Admin-only. Composes over [`CurrentSessionUser`] so bot tokens are
/// rejected at the auth step (401) rather than the role step (403).
/// The user row is re-read every request, so demotion takes effect on
/// the very next call without needing to purge sessions.
pub struct RequireAdmin(pub User);

#[async_trait]
impl FromRequestParts<AppState> for RequireAdmin {
    type Rejection = AppError;

    async fn from_request_parts(
        parts: &mut Parts,
        state: &AppState,
    ) -> Result<Self, Self::Rejection> {
        let CurrentSessionUser(user) =
            CurrentSessionUser::from_request_parts(parts, state).await?;
        if !user.is_admin {
            return Err(AppError::Forbidden);
        }
        Ok(RequireAdmin(user))
    }
}
