use crate::constants;
use crate::models::users::{User, UserID};
use crate::utils::rand_utils::rand_string;
use actix_session::Session;
use actix_web::dev::Payload;
use actix_web::{Error, FromRequest, HttpRequest};
use futures_util::future::{Ready, ready};
use std::net::IpAddr;

/// Money session errors
#[derive(thiserror::Error, Debug)]
enum MoneySessionError {
    #[error("Missing state!")]
    OIDCMissingState,
    #[error("Missing IP address!")]
    OIDCMissingIP,
    #[error("Invalid state!")]
    OIDCInvalidState,
    #[error("Invalid IP address!")]
    OIDCInvalidIP,
}

/// Money session
///
/// Basic wrapper around actix-session extractor
pub struct MoneySession(Session);

impl MoneySession {
    /// Generate OpenID state for this session
    pub fn gen_oidc_state(&self, ip: IpAddr) -> anyhow::Result<String> {
        let random_string = rand_string(50);
        self.0
            .insert(constants::sessions::OIDC_STATE_KEY, random_string.clone())?;
        self.0.insert(constants::sessions::OIDC_REMOTE_IP, ip)?;
        Ok(random_string)
    }

    /// Validate OpenID state
    pub fn validate_state(&self, state: &str, ip: IpAddr) -> anyhow::Result<()> {
        let session_state: String = self
            .0
            .get(constants::sessions::OIDC_STATE_KEY)?
            .ok_or(MoneySessionError::OIDCMissingState)?;

        let session_ip: IpAddr = self
            .0
            .get(constants::sessions::OIDC_REMOTE_IP)?
            .ok_or(MoneySessionError::OIDCMissingIP)?;

        if session_state != state {
            return Err(anyhow::anyhow!(MoneySessionError::OIDCInvalidState));
        }

        if session_ip != ip {
            return Err(anyhow::anyhow!(MoneySessionError::OIDCInvalidIP));
        }

        Ok(())
    }

    /// Set current user
    pub fn set_user(&self, user: &User) -> anyhow::Result<()> {
        self.0.insert(constants::sessions::USER_ID, user.id())?;
        Ok(())
    }

    /// Get current user
    pub fn current_user(&self) -> anyhow::Result<Option<UserID>> {
        Ok(self.0.get(constants::sessions::USER_ID)?)
    }

    /// Remove defined user
    pub fn unset_current_user(&self) -> anyhow::Result<()> {
        self.0.remove(constants::sessions::USER_ID);
        Ok(())
    }
}

impl FromRequest for MoneySession {
    type Error = Error;
    type Future = Ready<Result<Self, Error>>;

    #[inline]
    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
        ready(
            Session::from_request(req, &mut Payload::None)
                .into_inner()
                .map(MoneySession),
        )
    }
}
