use std::sync::Arc;

use axum::{
    extract::FromRequestParts,
    http::{header, request::Parts},
    response::Response,
};
use sqlx::PgPool;
use uuid::Uuid;

use crate::{access_token, blacklist::TokenBlacklist, Auth, AuthError, Claims};

/// Implemented by app state types that provide a database pool.
pub trait HasPool {
    fn pool(&self) -> &PgPool;
}

/// Implemented by app state types that provide an auth handle.
pub trait HasAuth {
    fn auth_handle(&self) -> Arc<Auth>;
}

/// Single request context bundling auth + pool + optional user identity.
///
/// Supports both JWT-based tokens and Sanctum-style personal access tokens.
/// For JWT tokens, `token_abilities` is `None` (all abilities granted).
/// For scoped access tokens, `token_abilities` holds the granted ability list.
pub struct Ctx {
    pub auth: Arc<Auth>,
    pub pool: PgPool,
    pub user: Option<Claims>,
    pub req_id: Uuid,
    /// Raw `Bearer` token from the `Authorization` header.
    ///
    /// Used by logout to blacklist the current access token.
    pub raw_bearer_token: Option<String>,
    /// `None` = JWT (unlimited abilities); `Some(vec)` = scoped access token.
    pub token_abilities: Option<Vec<String>>,
    /// Database ID of the current personal access token, if any.
    pub current_token_id: Option<i64>,
}

impl Ctx {
    pub fn user(&self) -> Option<&Claims> {
        self.user.as_ref()
    }

    /// Returns the claims or `Err(AuthError::InvalidToken)` for unauthenticated requests.
    pub fn require_auth(&self) -> Result<&Claims, AuthError> {
        self.user.as_ref().ok_or(AuthError::InvalidToken)
    }

    pub fn db(&self) -> &PgPool {
        &self.pool
    }

    /// `true` if the current token grants `ability`.
    ///
    /// JWT tokens (no scoped abilities) always return `true`.
    /// Access tokens check the stored ability list; `"*"` is a wildcard.
    pub fn token_can(&self, ability: &str) -> bool {
        match &self.token_abilities {
            None => true,
            Some(abilities) => abilities.iter().any(|a| a == "*" || a == ability),
        }
    }

    pub fn token_cant(&self, ability: &str) -> bool {
        !self.token_can(ability)
    }

    /// Returns `Err(AuthError::InvalidToken)` if unauthenticated, or
    /// `Err(AuthError::Forbidden)` if the token lacks `ability`.
    pub fn require_ability(&self, ability: &str) -> Result<(), AuthError> {
        self.require_auth()?;
        if self.token_can(ability) {
            Ok(())
        } else {
            Err(AuthError::Forbidden(ability.to_string()))
        }
    }
}

impl<S> FromRequestParts<S> for Ctx
where
    S: HasPool + HasAuth + Send + Sync,
{
    type Rejection = Response;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        let auth = state.auth_handle();
        let pool = state.pool().clone();
        let req_id = Uuid::new_v4();

        let mut token_abilities: Option<Vec<String>> = None;
        let mut current_token_id: Option<i64> = None;

        // Priority: pre-authenticated (BasicAuth/session middleware) → Bearer → None.
        let raw_token = extract_bearer_token(parts);
        let user = if let Some(claims) = parts.extensions.get::<Claims>().cloned() {
            Some(claims)
        } else if let Some(ref raw) = raw_token {
            if access_token::is_access_token(raw) {
                let mgr = access_token::AccessTokenManager::new(&pool);
                match mgr.find_by_token(raw).await {
                    Ok(Some(at)) if !at.is_expired() => {
                        let _ = mgr.touch(at.id).await;
                        token_abilities = Some(at.abilities);
                        current_token_id = Some(at.id);
                        Some(Claims::new(
                            at.tokenable_id.to_string(),
                            Vec::<String>::new(),
                        ))
                    }
                    _ => None,
                }
            } else {
                // JWT path: verify signature, then check blacklist.
                match auth.verify(raw) {
                    Ok(claims) if !TokenBlacklist::is_revoked(&pool, raw).await => Some(claims),
                    _ => None,
                }
            }
        } else {
            None
        };

        Ok(Ctx {
            auth,
            pool,
            user,
            req_id,
            raw_bearer_token: raw_token,
            token_abilities,
            current_token_id,
        })
    }
}

fn extract_bearer_token(parts: &Parts) -> Option<String> {
    parts
        .headers
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.strip_prefix("Bearer ").map(String::from))
}
