//! Authentication middleware and extractors.

use axum::{
    Json,
    extract::{FromRef, FromRequestParts},
    http::{StatusCode, request::Parts},
    response::{IntoResponse, Response},
};
use sqlx::PgPool;
use tower_sessions::Session;
use uuid::Uuid;

use chattyness_db::models::User;
use chattyness_error::ErrorResponse;

use super::session::SESSION_USER_ID_KEY;

/// Extractor for an authenticated user.
///
/// Returns 401 Unauthorized if the user is not authenticated.
pub struct AuthUser(pub User);

impl<S> FromRequestParts<S> for AuthUser
where
    S: Send + Sync,
    PgPool: FromRef<S>,
{
    type Rejection = AuthError;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        // Get session from request
        let session = Session::from_request_parts(parts, state)
            .await
            .map_err(|_| AuthError::SessionError)?;

        // Get user ID from session
        let user_id: Option<Uuid> = session
            .get(SESSION_USER_ID_KEY)
            .await
            .map_err(|_| AuthError::SessionError)?;

        let user_id = user_id.ok_or(AuthError::Unauthorized)?;

        // Get the database pool from state
        let pool = PgPool::from_ref(state);

        // Fetch the user from the database
        let user = chattyness_db::queries::users::get_user_by_id(&pool, user_id)
            .await
            .map_err(|_| AuthError::InternalError)?
            .ok_or(AuthError::Unauthorized)?;

        Ok(AuthUser(user))
    }
}

/// Extractor for an optional authenticated user.
///
/// Returns None if the user is not authenticated.
pub struct OptionalAuthUser(pub Option<User>);

impl<S> FromRequestParts<S> for OptionalAuthUser
where
    S: Send + Sync,
    PgPool: FromRef<S>,
{
    type Rejection = AuthError;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        match AuthUser::from_request_parts(parts, state).await {
            Ok(AuthUser(user)) => Ok(OptionalAuthUser(Some(user))),
            Err(AuthError::Unauthorized) => Ok(OptionalAuthUser(None)),
            Err(e) => Err(e),
        }
    }
}

/// Authentication errors.
#[derive(Debug)]
pub enum AuthError {
    Unauthorized,
    SessionError,
    InternalError,
}

impl IntoResponse for AuthError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AuthError::Unauthorized => (StatusCode::UNAUTHORIZED, "Authentication required"),
            AuthError::SessionError => (StatusCode::INTERNAL_SERVER_ERROR, "Session error"),
            AuthError::InternalError => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error"),
        };

        let body = ErrorResponse {
            error: message.to_string(),
            code: Some(format!("{:?}", self)),
        };

        (status, Json(body)).into_response()
    }
}

impl From<AuthError> for chattyness_error::AppError {
    fn from(err: AuthError) -> Self {
        match err {
            AuthError::Unauthorized => chattyness_error::AppError::Unauthorized,
            AuthError::SessionError => {
                chattyness_error::AppError::Internal("Session error".to_string())
            }
            AuthError::InternalError => {
                chattyness_error::AppError::Internal("Internal error".to_string())
            }
        }
    }
}
