use crate::app_config::AppConfig;
use crate::constants;
use crate::extractors::money_session::MoneySession;
use crate::models::tokens::{Token, TokenID};
use crate::models::users::{User, UserID};
use crate::services::{tokens_service, users_service};
use actix_remote_ip::RemoteIP;
use actix_web::dev::Payload;
use actix_web::error::ErrorPreconditionFailed;
use actix_web::{Error, FromRequest, HttpRequest};
use jwt_simple::algorithms::{HS256Key, MACLike};
use jwt_simple::common::VerificationOptions;
use jwt_simple::prelude::Duration;
use std::str::FromStr;

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct TokenClaims {
    #[serde(rename = "met")]
    pub method: String,
    pub uri: String,
}

#[derive(Debug, Clone)]
pub enum AuthenticatedMethod {
    /// User is authenticated using a cookie
    Cookie,
    /// User is authenticated through command line, for debugging purposes only
    Dev,
    /// User is authenticated using an API token
    Token(Token),
}

/// Authentication extractor. Extract authentication information from request
pub struct AuthExtractor {
    pub method: AuthenticatedMethod,
    pub user: User,
}

impl AuthExtractor {
    /// Get current user ID
    pub fn user_id(&self) -> UserID {
        self.user.id()
    }
}

impl FromRequest for AuthExtractor {
    type Error = Error;
    type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;

    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
        let req = req.clone();

        let remote_ip = match RemoteIP::from_request(&req, &mut Payload::None).into_inner() {
            Ok(ip) => ip,
            Err(e) => return Box::pin(async { Err(e) }),
        };

        Box::pin(async move {
            // Check for authentication using API token
            if let Some(token) = req.headers().get(constants::API_TOKEN_HEADER) {
                let Ok(jwt_token) = token.to_str() else {
                    return Err(actix_web::error::ErrorBadRequest(
                        "Failed to decode token as string!",
                    ));
                };

                let metadata = match jwt_simple::token::Token::decode_metadata(jwt_token) {
                    Ok(m) => m,
                    Err(e) => {
                        log::error!("Failed to decode JWT header metadata! {e}");
                        return Err(actix_web::error::ErrorBadRequest(
                            "Failed to decode JWT header metadata!",
                        ));
                    }
                };

                // Extract token ID
                let Some(kid) = metadata.key_id() else {
                    return Err(actix_web::error::ErrorBadRequest(
                        "Missing key id in request!",
                    ));
                };

                let token_id = match TokenID::from_str(kid) {
                    Ok(i) => i,
                    Err(e) => {
                        log::error!("Failed to parse token id! {e}");
                        return Err(actix_web::error::ErrorBadRequest(
                            "Failed to parse token id!",
                        ));
                    }
                };

                // Get token information
                let Ok(token) = tokens_service::get_by_id(token_id).await else {
                    log::error!("Token not found!");
                    return Err(actix_web::error::ErrorForbidden("Token not found!"));
                };

                // Decode JWT
                let key = HS256Key::from_bytes(token.token_value.as_ref());
                let verif = VerificationOptions {
                    max_validity: Some(Duration::from_secs(constants::API_TOKEN_JWT_MAX_DURATION)),
                    ..Default::default()
                };

                let claims = match key.verify_token::<TokenClaims>(jwt_token, Some(verif)) {
                    Ok(t) => t,
                    Err(e) => {
                        log::error!("JWT validation failed! {e}");
                        return Err(actix_web::error::ErrorForbidden("JWT validation failed!"));
                    }
                };

                // Check for nonce
                if claims.nonce.is_none() {
                    return Err(actix_web::error::ErrorBadRequest(
                        "A nonce is required in auth JWT!",
                    ));
                }

                // Check IP restriction
                if let Some(net) = token.ip_net()
                    && !net.contains(&remote_ip.0)
                {
                    log::error!(
                        "Trying to use token {:?} from unauthorized IP address: {remote_ip:?}",
                        token.id()
                    );
                    return Err(actix_web::error::ErrorForbidden(
                        "This token cannot be used from this IP address!",
                    ));
                }

                // Check for write access
                if token.read_only && !req.method().is_safe() {
                    return Err(actix_web::error::ErrorBadRequest(
                        "Read only token cannot perform write operations!",
                    ));
                }

                // Check for authorization
                let uri = req.uri().to_string();
                let authorized = (uri.starts_with("/api/account") && token.right_account)
                    || (uri.starts_with("/api/movement") && token.right_movement)
                    || (uri.starts_with("/api/inbox") && token.right_inbox)
                    || (uri.starts_with("/api/file") && token.right_file)
                    || (uri.starts_with("/api/auth/") && token.right_auth)
                    || (uri.starts_with("/api/stats") && token.right_stats)
                    || (uri.starts_with("/api/backup") && token.right_backup);

                if !authorized {
                    return Err(actix_web::error::ErrorBadRequest(
                        "This token cannot be used to query this route!",
                    ));
                }

                // Get user information
                let Ok(user) = users_service::get_user_by_id(token.user_id()) else {
                    return Err(actix_web::error::ErrorBadRequest(
                        "Failed to get user information from token!",
                    ));
                };

                // Update last use (if needed)
                if token.shall_update_time_used()
                    && let Err(e) = tokens_service::update_time_used(&token).await
                {
                    log::error!("Failed to refresh last usage of token! {e}");
                }

                // Handle tokens expiration
                if token.is_expired() {
                    log::error!("Attempted to use expired token! {token:?}");
                    return Err(actix_web::error::ErrorBadRequest("Token has expired!"));
                }

                return Ok(Self {
                    method: AuthenticatedMethod::Token(token),
                    user,
                });
            }

            // Check if login is hard-coded as program argument
            if let Some(email) = &AppConfig::get().unsecure_auto_login_email() {
                let user = users_service::get_user_by_email(email).map_err(|e| {
                    log::error!("Failed to retrieve dev user: {e}");
                    ErrorPreconditionFailed("Unable to retrieve dev user!")
                })?;
                return Ok(Self {
                    method: AuthenticatedMethod::Dev,
                    user,
                });
            }

            // Check for cookie authentication
            let session = MoneySession::extract(&req).await?;
            if let Some(user_id) = session.current_user().map_err(|e| {
                log::error!("Failed to retrieve user id: {e}");
                ErrorPreconditionFailed("Failed to read session information!")
            })? {
                let user = users_service::get_user_by_id(user_id).map_err(|e| {
                    log::error!("Failed to retrieve user from cookie session: {e}");
                    ErrorPreconditionFailed("Failed to retrieve user information!")
                })?;
                return Ok(Self {
                    method: AuthenticatedMethod::Cookie,
                    user,
                });
            };

            Err(ErrorPreconditionFailed("Authentication required!"))
        })
    }
}
