package com.airlock.iam.factor.infrastructure.rest.airlock2fa;

import com.airlock.iam.airlock2fa.api.domain.model.Airlock2FAAccountId;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.infrastructure.restclient.RestClient;
import com.airlock.iam.common.infrastructure.restclient.RestRequest;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAAuthDevice;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAAuthenticationException;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAMobileOnlyRedirectUri;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAPreAuthenticationData;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FASessionId;
import com.airlock.iam.factor.domain.model.airlock2fa.DeferredAuthenticationAnswer;
import com.airlock.iam.factor.domain.model.airlock2fa.ImmediateAuthenticationAnswer;
import com.airlock.iam.factor.domain.model.airlock2fa.futurae.Airlock2FAFuturaeAuthSession;
import com.airlock.iam.factor.domain.model.airlock2fa.futurae.FuturaeErrorResponseException;
import com.airlock.iam.factor.domain.model.airlock2fa.otp.Airlock2FAOtp;
import com.airlock.iam.factor.domain.model.airlock2fa.otp.Airlock2FAPasscode;
import com.airlock.iam.factor.domain.model.airlock2fa.qrcode.Airlock2FAExchangeTokensAnswer;
import com.airlock.iam.factor.domain.model.airlock2fa.qrcode.Airlock2FAQrCodeImage;
import com.airlock.iam.factor.domain.model.airlock2fa.qrcode.Airlock2FAQrCodeUrl;
import com.airlock.iam.factor.domain.service.airlock2fa.Airlock2FAAuthenticationService;
import com.airlock.iam.factor.domain.service.airlock2fa.Airlock2FAPreAuthenticationService;
import com.airlock.iam.factor.domain.service.airlock2fa.QrCodeImageReader;
import com.airlock.iam.factor.domain.service.airlock2fa.SessionBasedAuthenticationAnswer;
import com.airlock.iam.factor.infrastructure.rest.airlock2fa.futurae.Airlock2FAPreAuthenticationServiceImpl;
import com.airlock.iam.factor.infrastructure.rest.airlock2fa.futurae.FuturaeAuthenticationAnswerReaders;
import com.airlock.iam.factor.infrastructure.rest.airlock2fa.futurae.FuturaeAuthenticationRequestFactory;
import com.airlock.iam.factor.infrastructure.rest.airlock2fa.futurae.FuturaeExchangeTokenAnswerReaders;
import com.airlock.iam.flow.shared.api.domain.model.message.Airlock2FAMessage;
import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.time.Duration;
import java.util.Optional;

public class Airlock2FAAuthenticationServiceImpl implements Airlock2FAAuthenticationService {
   private static final ELogger LOG = ELoggerFactory.getLogger(Airlock2FAAuthenticationServiceImpl.class);
   private final Provider<RestClient> restClient;
   private final Provider<FuturaeAuthenticationRequestFactory> requestFactory;
   private final Provider<QrCodeImageReader> qrCodeImageReader;
   private final Provider<FuturaeAuthenticationAnswerReaders> answerReaders;
   private final Provider<FuturaeExchangeTokenAnswerReaders> exchangeTokenAnswerReaders;
   private final Provider<Airlock2FAPreAuthenticationServiceImpl> preAuthenticationService;

   @Inject
   public Airlock2FAAuthenticationServiceImpl(
      Provider<RestClient> restClient,
      Provider<FuturaeAuthenticationRequestFactory> requestFactory,
      Provider<FuturaeAuthenticationAnswerReaders> answerReaders,
      Provider<FuturaeExchangeTokenAnswerReaders> exchangeTokenAnswerReaders,
      Provider<QrCodeImageReader> qrCodeImageReader,
      Provider<Airlock2FAPreAuthenticationServiceImpl> preAuthenticationService
   ) {
      this.restClient = restClient;
      this.requestFactory = requestFactory;
      this.answerReaders = answerReaders;
      this.exchangeTokenAnswerReaders = exchangeTokenAnswerReaders;
      this.qrCodeImageReader = qrCodeImageReader;
      this.preAuthenticationService = preAuthenticationService;
   }

   @Override
   public Airlock2FAPreAuthenticationService.PreAuthenticationAnswer preAuthenticate(Airlock2FAPreAuthenticationData preAuthenticationData) throws Airlock2FAAuthenticationException {
      try {
         return ((Airlock2FAPreAuthenticationServiceImpl)this.preAuthenticationService.get()).preAuthenticate(preAuthenticationData);
      } catch (Airlock2FAAuthenticationException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not pre-authenticate user with Airlock 2FA account %s", preAuthenticationData.accountId()), e
         );
      }
   }

   @Override
   public DeferredAuthenticationAnswer.UsernamelessQrCodeDeferredAuthenticationAnswer authenticateWithUsernamelessQrCode(
      Duration sessionTimeout, Optional<Airlock2FAMessage> message
   ) throws Airlock2FAAuthenticationException {
      LOG.debug("Authenticating with Usernameless QR Code.");

      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get()).authenticateWithUsernamelessQrCode(sessionTimeout, message);
         return ((RestClient)this.restClient.get())
            .read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forUsernamelessQrCode(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException("Could not authenticate with Usernameless QR Code", e);
      }
   }

   @Override
   public ImmediateAuthenticationAnswer authenticateWithPasscode(Airlock2FAAccountId accountId, Airlock2FAPasscode passcode) throws Airlock2FAAuthenticationException {
      LOG.debug("Authenticating account with Passcode: " + accountId);

      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get()).authenticateWithPasscodeRequestFor(accountId, passcode);
         return ((RestClient)this.restClient.get()).read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forPasscode(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not authenticate user with Airlock 2FA account %s with passcode", accountId), e
         );
      }
   }

   @Override
   public SessionBasedAuthenticationAnswer.OneTouchAuthenticationAnswer authenticateWithOneTouch(
      Airlock2FAAccountId accountId, Airlock2FAAuthDevice device, Airlock2FAMessage message
   ) throws Airlock2FAAuthenticationException {
      LOG.debug("Authenticating account with One-Touch: " + accountId);

      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get()).authenticateWithOneTouchRequestFor(accountId, device, message);
         return ((RestClient)this.restClient.get()).read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forOneTouch(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not authenticate user with Airlock 2FA account %s with One-Touch notification", accountId), e
         );
      }
   }

   @Override
   public SessionBasedAuthenticationAnswer.OneTouchAuthenticationAnswer authenticateWithPushToAll(Airlock2FAAccountId accountId, Airlock2FAMessage message) throws Airlock2FAAuthenticationException {
      LOG.debug("Authenticating account with One-Touch (Push-to-All): " + accountId);

      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get()).authenticateWithPushToAllRequestFor(accountId, message);
         return ((RestClient)this.restClient.get()).read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forOneTouch(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not authenticate user with Airlock 2FA account %s with One-Touch (Push-to-All) notification", accountId), e
         );
      }
   }

   @Override
   public SessionBasedAuthenticationAnswer.OnlineQrCodeAuthenticationAnswer authenticateWithOnlineQrCode(
      Airlock2FAAccountId accountId, Optional<Airlock2FAMessage> message, Optional<Duration> sessionTimeout
   ) throws Airlock2FAAuthenticationException {
      LOG.debug("Authenticating account with online QR Code: " + accountId);

      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get())
            .authenticateWithOnlineQrCodeRequestFor(accountId, Optional.empty(), message, sessionTimeout);
         return ((RestClient)this.restClient.get()).read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forOnlineQrCode(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not authenticate user with Airlock 2FA account %s with Online QR Code", accountId), e
         );
      }
   }

   @Override
   public SessionBasedAuthenticationAnswer.MobileOnlyAuthenticationAnswer authenticateWithMobileOnly(
      Airlock2FAAccountId accountId, Optional<Airlock2FAMobileOnlyRedirectUri> mobileOnlyRedirectUri
   ) throws Airlock2FAAuthenticationException {
      LOG.debug("Authenticating account with Mobile Only: " + accountId);

      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get())
            .authenticateWithMobileAuthRequestFor(accountId, mobileOnlyRedirectUri, Optional.empty());
         return ((RestClient)this.restClient.get()).read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forMobileOnly(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not authenticate user with Airlock 2FA account %s with Mobile Only", accountId), e
         );
      }
   }

   @Override
   public SessionBasedAuthenticationAnswer.OfflineQrCodeAuthenticationAnswer authenticateWithOfflineQrCode(
      Airlock2FAAccountId accountId, Airlock2FAAuthDevice device, Optional<Airlock2FAMessage> message
   ) {
      LOG.debug("Authenticating account with Offline QR Code: " + accountId);

      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get())
            .authenticateWithOfflineQrCodeRequestFor(accountId, device, message);
         return ((RestClient)this.restClient.get()).read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forOfflineQrCode(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not authenticate user with Airlock 2FA account %s with Mobile Only", accountId), e
         );
      }
   }

   @Override
   public Airlock2FAQrCodeImage retrieveOfflineQrCodeImage(Airlock2FAAccountId accountId, Airlock2FAQrCodeUrl url) throws Airlock2FAAuthenticationException {
      LOG.debug("Retrieving Offline QR Code for account: " + accountId);
      return ((QrCodeImageReader)this.qrCodeImageReader.get()).retrieveFrom(url.getUrl());
   }

   @Override
   public Result<Airlock2FAAuthenticationService.ValidateOfflineQrCodeAnswer, FuturaeErrorResponseException> validateOfflineQrCode(
      Airlock2FAAccountId accountId, Airlock2FASessionId sessionId, Airlock2FAOtp otp
   ) {
      LOG.debug("Validating Offline QR Code OTP for account: " + accountId);

      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get()).validateOfflineQrCode(accountId, sessionId, otp);
         return ((RestClient)this.restClient.get())
            .read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forValidateOfflineQrCode(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not validate OTP for user with Airlock 2FA account %s", accountId), e
         );
      }
   }

   @Override
   public Airlock2FAAuthenticationService.AuthenticationStatusAnswer checkAuthenticationStatus(Airlock2FAFuturaeAuthSession futuraeSession) throws Airlock2FAAuthenticationException {
      Airlock2FAAccountId accountId = futuraeSession.getAccountId();

      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get())
            .checkAuthStatusRequestFor(accountId, futuraeSession.getSessionId());
         Result<Airlock2FAAuthenticationService.AuthenticationStatusAnswer, FuturaeErrorResponseException> result = ((RestClient)this.restClient.get())
            .read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forAuthStatusOrError(request));
         return result.orMapFailure(Airlock2FAAuthenticationServiceImpl::handleStatusCheckFailure);
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not check authentication status of user with Airlock 2FA account %s", accountId), e
         );
      }
   }

   private static Airlock2FAAuthenticationService.AuthenticationStatusAnswer handleStatusCheckFailure(FuturaeErrorResponseException exception) {
      if (exception.type() == FuturaeErrorResponseException.FuturaeErrorResponseType.BAD_REQUEST) {
         LOG.info("Bad request while checking authentication status, probably because the session has timed out.");
         return new Airlock2FAAuthenticationService.AuthenticationStatusAnswer(Airlock2FAAuthenticationService.AuthenticationStatusResult.TIMEOUT);
      } else {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            "Unexpected failure response while checking Airlock 2FA authentication status", exception
         );
      }
   }

   @Override
   public Result<Airlock2FAAuthenticationService.AuthenticationStatusAnswer, FuturaeErrorResponseException> checkAuthenticationStatusUsernameless(
      Airlock2FASessionId sessionId
   ) {
      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get()).checkAuthStatusRequestFor(sessionId);
         return ((RestClient)this.restClient.get()).read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forAuthStatusOrError(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException("Could not check authentication status of usernameless authentication", e);
      }
   }

   @Override
   public Result<ImmediateAuthenticationAnswer, FuturaeErrorResponseException> abortAuthentication(Airlock2FAAccountId accountId, Airlock2FASessionId sessionId) {
      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get()).abortAuthenticationRequestFor(accountId, sessionId);
         return ((RestClient)this.restClient.get()).read(request, ((FuturaeAuthenticationAnswerReaders)this.answerReaders.get()).forAbort(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not abort authentication or transaction for user with Airlock 2FA account %s", accountId), e
         );
      }
   }

   @Override
   public Airlock2FAExchangeTokensAnswer fetchAuthenticationExchangeTokens(
      Airlock2FAAccountId accountId,
      Airlock2FASessionId sessionId,
      Duration exchangeTokenLifetime,
      int totalExchangeTokens,
      String uriSchema,
      String associatedDomain
   ) {
      try {
         RestRequest request = ((FuturaeAuthenticationRequestFactory)this.requestFactory.get())
            .fetchAuthenticationExchangeTokensRequest(accountId, sessionId, exchangeTokenLifetime, totalExchangeTokens, uriSchema, associatedDomain);
         return ((RestClient)this.restClient.get())
            .read(request, ((FuturaeExchangeTokenAnswerReaders)this.exchangeTokenAnswerReaders.get()).forSessionExchangeTokens(request));
      } catch (JsonProcessingException e) {
         throw Airlock2FAAuthenticationException.airlock2FAAuthenticationException(
            String.format("Could not fetch exchange tokens for user with Airlock 2FA account %s", accountId), e
         );
      }
   }
}
