/*!
   Module `service` provides the canonical implementation of the [ApiService] port. All
   user-domain logic is defined here.
*/
use avam_protocol::Packets;
use axum_session::SessionAnySession;
use dashmap::DashMap;
use tokio::sync::mpsc::Sender;

use crate::inbound::http::handlers::oauth::AuthorizationCodeRequest;
use crate::inbound::http::handlers::oauth::GrantType;
use crate::inbound::http::handlers::oauth::VerifyClientAuthorizationRequest;

use super::models::oauth::Client;
use super::models::oauth::*;
use super::models::user::*;

use super::ports::{ApiService, OAuthRepository, UserNotifier, UserRepository};

// pub trait Repository = UserRepository + OAuthRepository;
pub trait Email = UserNotifier;

#[derive(Debug, Clone)]
pub struct Service<R, N>
where
    R: UserRepository + OAuthRepository,
    N: Email,
{
    repo: R,
    notifier: N,
    connections: DashMap<uuid::Uuid, Sender<Packets>>,
}

impl<R, N> Service<R, N>
where
    R: UserRepository + OAuthRepository,
    N: Email,
{
    pub fn new(repo: R, notifier: N) -> Self {
        Self {
            repo,
            notifier,
            connections: DashMap::new(),
        }
    }
}

impl<R, N> ApiService for Service<R, N>
where
    R: UserRepository + OAuthRepository,
    N: Email,
{
    fn add_connection(&self, user: &User, sender: Sender<Packets>) {
        self.connections.insert(user.id(), sender);
    }

    fn has_connection(&self, user: &User) -> bool {
        self.connections.contains_key(&user.id())
    }

    fn remove_connection(&self, user: &User) {
        self.connections.remove(&user.id());
    }

    async fn send(&self, user: &User, packet: Packets) -> Result<(), anyhow::Error> {
        if let Some(sender) = self.connections.get(&user.id()) {
            sender.send(packet).await?;
        }
        Ok(())
    }

    async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError> {
        let result = self.repo.create_user(req).await;

        #[allow(clippy::question_mark)]
        if result.is_err() {
            // something went wrong, log the error
            // but keep passing on the result to the requester (http server)
            return result;
        }

        let user = result.as_ref().unwrap();

        let token = self.repo.create_activation_token(user).await?;

        // generate a activation token and send an email
        self.notifier.user_created(user, &token).await;

        result
    }

    async fn get_user_session(&self, session: &SessionAnySession) -> Option<User> {
        let user_id = session.get("user")?;

        self.repo.find_user_by_id(user_id).await.unwrap_or(None)
    }

    async fn activate_user_account(
        &self,
        token: ActivationToken,
    ) -> Result<User, ActivateUserError> {
        let user = match self.repo.find_user_by_activation_token(&token).await {
            Ok(u) => u,
            Err(e) => return Err(ActivateUserError::Unknown(e)),
        };

        let Some(user) = user else {
            return Err(ActivateUserError::NotFound { token });
        };

        let req = UpdateUserRequest::new().set_verified(Verified::new(true));

        let update = self.repo.update_user(&user, req).await;

        if let Err(e) = update {
            return Err(ActivateUserError::Unknown(e.into()));
        }

        self.repo.delete_activation_token_for_user(&user).await?;

        let (_, user) = update.unwrap();

        Ok(user)
    }

    async fn user_login(&self, req: UserLoginRequest) -> Result<User, UserLoginError> {
        let email = req.email();

        let user = match self.repo.find_user_by_email(email).await {
            Ok(u) => u,
            Err(e) => {
                tracing::error!("{:#?}", e);
                return Err(UserLoginError::Unknown(e));
            }
        };

        let Some(user) = user else {
            tracing::warn!("User not found");
            return Err(UserLoginError::NotFound {
                email: email.clone(),
            });
        };

        if !user.verified().is_verified() {
            return Err(UserLoginError::NotActive {
                email: email.clone(),
            });
        }

        if let Err(e) = user.password().verify(req.password()) {
            tracing::warn!("{:#?}", e);
            return Err(UserLoginError::NotFound {
                email: email.clone(),
            });
        }

        // Invalidate any reset tokens that might exist
        let _ = self.repo.delete_password_reset_tokens_for_user(&user).await;

        Ok(user)
    }

    async fn forgot_password(&self, email: &EmailAddress) {
        let user = match self.repo.find_user_by_email(email).await {
            Ok(u) => u,
            Err(e) => {
                tracing::error!("{:#?}", e);
                return;
            }
        };

        let Some(user) = user else {
            tracing::warn!("User not found");
            return;
        };

        let token = match self.repo.create_password_reset_token(&user).await {
            Ok(t) => t,
            Err(e) => {
                tracing::error!("{:#?}", e);
                return;
            }
        };

        self.notifier.forgot_password(&user, &token).await;
    }

    async fn reset_password(
        &self,
        token: &PasswordResetToken,
        password: &Password,
    ) -> Result<User, ResetPasswordError> {
        let user = match self.repo.find_user_by_password_reset_token(token).await {
            Ok(u) => u,
            Err(e) => {
                tracing::error!("{:#?}", e);
                return Err(ResetPasswordError::Unknown(e));
            }
        };

        let Some(user) = user else {
            tracing::warn!("User not found");
            return Err(ResetPasswordError::NotFound);
        };

        let req = UpdateUserRequest::new().set_password(password.clone());

        self.repo
            .update_user(&user, req)
            .await
            .map_err(|e| ResetPasswordError::Unknown(e.into()))?;

        // Invalidate any reset tokens that might exist
        let _ = self.repo.delete_password_reset_tokens_for_user(&user).await;

        Ok(user)
    }

    async fn find_user_by_password_reset_token(&self, token: &PasswordResetToken) -> Option<User> {
        self.repo
            .find_user_by_password_reset_token(token)
            .await
            .unwrap_or(None)
    }

    async fn find_client_by_id(&self, id: uuid::Uuid) -> Option<Client> {
        self.repo.find_client_by_id(id).await.ok().flatten()
    }

    async fn generate_authorization_code(
        &self,
        user: &User,
        req: AuthorizeRequest,
    ) -> Result<AuthorizationResponse, anyhow::Error> {
        let Some(client) = self.repo.find_client_by_id(req.client_id()).await? else {
            return Err(anyhow::anyhow!("Client not found"));
        };

        if client.redirect_uri() != &req.redirect_uri() {
            return Err(anyhow::anyhow!("Invalid redirect uri"));
        }

        let code = self
            .repo
            .create_authorization_code(
                user.id(),
                client.id(),
                req.code_challenge(),
                req.code_challenge_method().unwrap_or_default(),
            )
            .await?;

        Ok(AuthorizationResponse::new(code, req.state()))
    }

    async fn create_token(
        &self,
        req: AuthorizationCodeRequest,
    ) -> Result<TokenSubject, TokenError> {
        if req.grant_type() != GrantType::AuthorizationCode {
            return Err(TokenError::InvalidRequest);
        }

        let code = req.code();

        let Some(token) = self.repo.get_token_subject(code).await? else {
            return Err(TokenError::InvalidRequest);
        };

        let code_verifier = req.code_verifier();
        let code_challenge = match token.code_challenge_method() {
            CodeChallengeMethod::Plain => {
                use base64::prelude::*;
                BASE64_URL_SAFE_NO_PAD.encode(code_verifier.to_string())
            }
            CodeChallengeMethod::Sha256 => {
                use base64::prelude::*;
                BASE64_URL_SAFE_NO_PAD.encode(sha256::digest(code_verifier.to_string()))
            }
        };

        if token.code_challenge() != code_challenge {
            return Err(TokenError::InvalidRequest);
        }

        let Some(client) = self.repo.find_client_by_id(token.client_id()).await? else {
            return Err(TokenError::InvalidRequest); // no such client
        };

        if &req.redirect_uri() != client.redirect_uri() {
            return Err(TokenError::InvalidRequest); // invalid redirect uri
        }

        let _ = self.repo.delete_token(req.code()).await;

        Ok(token)
    }

    async fn verify_client_authorization(
        &self,
        req: VerifyClientAuthorizationRequest,
    ) -> Result<User, anyhow::Error> {
        let user_id = req.user_id();
        let client_id = req.client_id();

        if !self.repo.is_authorized_client(user_id, client_id).await? {
            return Err(anyhow::anyhow!("Unauthorized"));
        }

        let Some(user) = self.repo.find_user_by_id(user_id).await? else {
            return Err(anyhow::anyhow!("Unauthorized"));
        };

        Ok(user)
    }
}
