//! # User tokens management

use crate::connections::redis_connection;
use crate::constants::{
    AUTH_TOKEN_DURATION, AUTH_TOKEN_HB_MIN_INTERVAL, AUTH_TOKEN_MAX_INACTIVITY,
};
use crate::models::{User, UserID};
use crate::utils::string_utils::rand_str;
use crate::utils::time_utils::time;
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
use std::future::{Ready, ready};

#[derive(thiserror::Error, Debug)]
enum LoginTokenServiceError {
    #[error("Malformed login token!")]
    MalformedToken,
}

#[derive(Debug, Clone)]
pub struct LoginTokenValue(String);

impl LoginTokenValue {
    pub fn parse(&self) -> anyhow::Result<(UserID, &str)> {
        let (id, key) = self
            .0
            .split_once('-')
            .ok_or(LoginTokenServiceError::MalformedToken)?;

        Ok((UserID(id.parse()?), key))
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct LoginToken {
    #[serde(rename = "e")]
    expire: u64,
    #[serde(rename = "h")]
    hb: u64,
    #[serde(rename = "t")]
    key: String,
    #[serde(rename = "u")]
    pub user_id: UserID,
}

impl LoginToken {
    fn new(user_id: UserID) -> Self {
        Self {
            expire: time() + AUTH_TOKEN_DURATION.as_secs(),
            hb: time(),
            key: rand_str(40),
            user_id,
        }
    }

    fn is_expired(&self) -> bool {
        self.expire < time() || self.hb + AUTH_TOKEN_MAX_INACTIVITY.as_secs() < time()
    }

    fn refresh_hb(&mut self) -> bool {
        if self.hb + AUTH_TOKEN_HB_MIN_INTERVAL.as_secs() < time() {
            self.hb = time();
            return true;
        }
        false
    }

    /// Get the token returned to the user
    fn key(&self) -> String {
        format!("{}-{}", self.user_id.0, self.key)
    }
}

/// Get redis key for a given user
fn redis_key(user_id: &UserID) -> String {
    format!("tok-{}", user_id.0)
}

/// Get the tokens of a user
async fn get_user_tokens(user_id: UserID) -> anyhow::Result<Vec<LoginToken>> {
    Ok(redis_connection::get_value(&redis_key(&user_id))
        .await?
        .unwrap_or_default())
}

async fn set_user_tokens(user_id: UserID, mut tokens: Vec<LoginToken>) -> anyhow::Result<()> {
    tokens.retain(|t| !t.is_expired());

    redis_connection::set_value(&redis_key(&user_id), &tokens, AUTH_TOKEN_MAX_INACTIVITY).await
}

/// Generate a new login token
pub async fn gen_new_token(user: &User) -> anyhow::Result<String> {
    let mut tokens = get_user_tokens(user.id()).await?;

    let token = LoginToken::new(user.id());
    let key = token.key();

    tokens.push(token);
    set_user_tokens(user.id(), tokens).await?;

    Ok(key)
}

/// Delete a login token (disconnect a client)
pub async fn delete_token(token: &LoginTokenValue) -> anyhow::Result<()> {
    let (user_id, key) = token.parse()?;
    let mut tokens = get_user_tokens(user_id).await?;

    tokens.retain(|t| t.key != key);

    set_user_tokens(user_id, tokens).await
}

/// Remove all the tokens of a user (disconnect it from all devices)
pub async fn disconnect_user_from_all_devices(user_id: UserID) -> anyhow::Result<()> {
    set_user_tokens(user_id, vec![]).await
}

/// Get a user information from its token
async fn load_token_info(token: &LoginTokenValue) -> anyhow::Result<Option<LoginToken>> {
    let (user_id, key) = token.parse()?;
    let mut user_tokens = get_user_tokens(user_id).await?;

    let token = match user_tokens.iter_mut().find(|t| t.key == key) {
        Some(t) => t,
        None => {
            log::error!("Could not find token for key '{key}' (missing token)");
            return Ok(None);
        }
    };

    if token.is_expired() {
        log::error!("Could not find token for key '{key}' (token expired)");
        return Ok(None);
    }

    // Check if heartbeat must be updated
    let clone = token.clone();
    if token.refresh_hb() {
        set_user_tokens(clone.user_id, user_tokens).await?;
    }

    Ok(Some(clone))
}

impl FromRequest for LoginTokenValue {
    type Error = actix_web::Error;
    type Future = Ready<Result<Self, Self::Error>>;

    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
        ready(match req.headers().get("X-Auth-Token") {
            Some(v) => Ok(LoginTokenValue(v.to_str().unwrap_or("").to_string())),
            None => Err(actix_web::error::ErrorBadRequest(
                "Missing auth token header!",
            )),
        })
    }
}

impl FromRequest for LoginToken {
    type Error = actix_web::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();
        Box::pin(async move {
            let token = LoginTokenValue::extract(&req).await?;

            let token = match load_token_info(&token).await {
                Err(e) => {
                    log::error!("Failed to load auth token! {e}");
                    return Err(actix_web::error::ErrorPreconditionFailed(
                        "Failed to check auth token!",
                    ));
                }
                Ok(None) => {
                    return Err(actix_web::error::ErrorPreconditionFailed(
                        "Invalid auth token!",
                    ));
                }
                Ok(Some(t)) => t,
            };

            Ok(token)
        })
    }
}
