use super::AuthProvider;
use crate::auth::AuthenticationError;
use crate::database::models::user_item;
use crate::database::redis::RedisPool;
use crate::models::pats::Scopes;
use crate::models::users::User;
use crate::queue::session::AuthQueue;
use crate::routes::internal::session::get_session_metadata;
use actix_web::HttpRequest;
use actix_web::http::header::{AUTHORIZATION, HeaderValue};
use chrono::Utc;

pub async fn get_maybe_user_from_headers<'a, E>(
    req: &HttpRequest,
    executor: E,
    redis: &RedisPool,
    session_queue: &AuthQueue,
    required_scopes: Scopes,
) -> Result<Option<(Scopes, User)>, AuthenticationError>
where
    E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
    if !req.headers().contains_key(AUTHORIZATION) {
        return Ok(None);
    }

    // Fetch DB user record and minos user from headers
    let Some((scopes, db_user)) = get_user_record_from_bearer_token(
        req,
        None,
        executor,
        redis,
        session_queue,
    )
    .await?
    else {
        return Ok(None);
    };

    if !scopes.contains(required_scopes) {
        return Ok(None);
    }

    Ok(Some((scopes, User::from_full(db_user))))
}

pub async fn get_user_from_headers<'a, E>(
    req: &HttpRequest,
    executor: E,
    redis: &RedisPool,
    session_queue: &AuthQueue,
    required_scopes: Scopes,
) -> Result<(Scopes, User), AuthenticationError>
where
    E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
    // Fetch DB user record and minos user from headers
    let (scopes, db_user) = get_user_record_from_bearer_token(
        req,
        None,
        executor,
        redis,
        session_queue,
    )
    .await?
    .ok_or_else(|| AuthenticationError::InvalidCredentials)?;

    let user = User::from_full(db_user);

    if !scopes.contains(required_scopes) {
        return Err(AuthenticationError::InvalidCredentials);
    }

    Ok((scopes, user))
}

pub async fn get_user_record_from_bearer_token<'a, 'b, E>(
    req: &HttpRequest,
    token: Option<&str>,
    executor: E,
    redis: &RedisPool,
    session_queue: &AuthQueue,
) -> Result<Option<(Scopes, user_item::DBUser)>, AuthenticationError>
where
    E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
    let token = if let Some(token) = token {
        token
    } else {
        extract_authorization_header(req)?
    };

    let possible_user = match token.split_once('_') {
        Some(("mrp", _)) => {
            let pat =
                crate::database::models::pat_item::DBPersonalAccessToken::get(
                    token, executor, redis,
                )
                .await?
                .ok_or_else(|| AuthenticationError::InvalidCredentials)?;

            if pat.expires < Utc::now() {
                return Err(AuthenticationError::InvalidCredentials);
            }

            let user =
                user_item::DBUser::get_id(pat.user_id, executor, redis).await?;

            session_queue.add_pat(pat.id).await;

            user.map(|x| (pat.scopes, x))
        }
        Some(("mra", _)) => {
            let session =
                crate::database::models::session_item::DBSession::get(
                    token, executor, redis,
                )
                .await?
                .ok_or_else(|| AuthenticationError::InvalidCredentials)?;

            if session.expires < Utc::now() {
                return Err(AuthenticationError::InvalidCredentials);
            }

            let user =
                user_item::DBUser::get_id(session.user_id, executor, redis)
                    .await?;

            let rate_limit_ignore = dotenvy::var("RATE_LIMIT_IGNORE_KEY")?;
            if req
                .headers()
                .get("x-ratelimit-key")
                .and_then(|x| x.to_str().ok())
                .is_none_or(|x| x != rate_limit_ignore)
            {
                let metadata = get_session_metadata(req).await?;
                session_queue.add_session(session.id, metadata).await;
            }

            user.map(|x| (Scopes::all(), x))
        }
        Some(("mro", _)) => {
            use crate::database::models::oauth_token_item::DBOAuthAccessToken;

            let hash = DBOAuthAccessToken::hash_token(token);
            let access_token =
                crate::database::models::oauth_token_item::DBOAuthAccessToken::get(hash, executor)
                    .await?
                    .ok_or(AuthenticationError::InvalidCredentials)?;

            if access_token.expires < Utc::now() {
                return Err(AuthenticationError::InvalidCredentials);
            }

            let user = user_item::DBUser::get_id(
                access_token.user_id,
                executor,
                redis,
            )
            .await?;

            session_queue.add_oauth_access_token(access_token.id).await;

            user.map(|u| (access_token.scopes, u))
        }
        Some(("github" | "gho" | "ghp", _)) => {
            let user = AuthProvider::GitHub.get_user(token).await?;
            let id =
                AuthProvider::GitHub.get_user_id(&user.id, executor).await?;

            let user = user_item::DBUser::get_id(
                id.ok_or_else(|| AuthenticationError::InvalidCredentials)?,
                executor,
                redis,
            )
            .await?;

            user.map(|x| ((Scopes::all() ^ Scopes::restricted()), x))
        }
        _ => return Err(AuthenticationError::InvalidAuthMethod),
    };

    Ok(possible_user)
}

pub fn extract_authorization_header(
    req: &HttpRequest,
) -> Result<&str, AuthenticationError> {
    let headers = req.headers();
    let token_val: Option<&HeaderValue> = headers.get(AUTHORIZATION);
    let token_val = token_val
        .ok_or_else(|| AuthenticationError::InvalidAuthMethod)?
        .to_str()
        .map_err(|_| AuthenticationError::InvalidCredentials)?;
    Ok(if let Some(token) = token_val.strip_prefix("Bearer ") {
        token
    } else {
        token_val
    })
}

pub async fn check_is_moderator_from_headers<'a, 'b, E>(
    req: &HttpRequest,
    executor: E,
    redis: &RedisPool,
    session_queue: &AuthQueue,
    required_scopes: Scopes,
) -> Result<User, AuthenticationError>
where
    E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
    let user = get_user_from_headers(
        req,
        executor,
        redis,
        session_queue,
        required_scopes,
    )
    .await?
    .1;

    if user.role.is_mod() {
        Ok(user)
    } else {
        Err(AuthenticationError::InvalidCredentials)
    }
}
