package com.airlock.iam.flow.shared.domain.service.oath;

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.api.domain.model.persistency.exception.NotFoundException;
import com.airlock.iam.common.api.domain.model.persistency.exception.PersisterException;
import com.airlock.iam.common.api.domain.model.store.user.exception.UserEntityNotFoundException;
import com.airlock.iam.common.domain.model.event.OathOtpDeviceActivatedEventFactory;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
import com.airlock.iam.core.misc.impl.persistency.CredentialBeanImpl;
import com.airlock.iam.core.misc.impl.persistency.CredentialData;
import com.airlock.iam.core.misc.impl.tokenverifier.oathotp.OathOtpSettings;
import com.airlock.iam.core.misc.impl.tokenverifier.oathotp.OathTokenVerifierDelegate;
import com.airlock.iam.core.misc.impl.tokenverifier.oathotp.OtpCredBeanData;
import com.airlock.iam.core.misc.persistency.CredentialBean;
import com.airlock.iam.core.misc.tokenverifier.TokenVerifierException;
import com.airlock.iam.core.misc.tokenverifier.TokenVerifierResponse;
import com.airlock.iam.core.misc.util.oathotp.OathOtpCredentialGenerator;
import com.airlock.iam.core.misc.util.oathotp.OathOtpUtilities;
import com.airlock.iam.core.misc.util.qrcode.QrCodeUtilities;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.shared.domain.model.oath.OathOtpActivationStepSession;
import com.airlock.iam.flow.shared.domain.model.oath.OathOtpActivationStepSettings;
import com.airlock.iam.flow.types.api.domain.model.session.KnownMultiFlowType;
import com.airlock.iam.flow.types.api.domain.model.session.MultiFlowType;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.security.util.crypto.DecryptionException;
import com.airlock.iam.security.util.crypto.EncryptionException;
import com.google.common.annotations.VisibleForTesting;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.awt.image.BufferedImage;
import java.net.URI;
import java.util.Objects;
import java.util.Optional;

public class OathOtpActivationService {
   private static final ELogger LOG = ELoggerFactory.getLogger(OathOtpActivationService.class);
   private final Provider<OathOtpSettings> oathOtpSettings;
   private final Provider<OathOtpActivationStepSettings> settings;
   private final Provider<PrincipalInfo> user;
   private final Provider<OathOtpActivationStepSession> stepSession;
   private final Provider<OathOtpPersistingService> persistingService;
   private final Provider<MultiFlowType> flowType;
   private final Provider<OathOtpDeviceActivatedEventFactory> eventFactory;
   private final Provider<OathTokenVerifierDelegate> oathTokenVerifier;
   private final Provider<HelpdeskLogger> helpdeskLogger;

   @Inject
   public OathOtpActivationService(
      Provider<OathOtpSettings> oathOtpSettings,
      Provider<OathOtpActivationStepSettings> settings,
      Provider<PrincipalInfo> user,
      Provider<OathOtpActivationStepSession> stepSession,
      Provider<OathOtpPersistingService> persistingService,
      Provider<MultiFlowType> flowType,
      Provider<OathOtpDeviceActivatedEventFactory> eventFactory,
      Provider<OathTokenVerifierDelegate> oathTokenVerifier,
      Provider<HelpdeskLogger> helpdeskLogger
   ) {
      this.oathOtpSettings = oathOtpSettings;
      this.settings = settings;
      this.user = user;
      this.stepSession = stepSession;
      this.persistingService = persistingService;
      this.flowType = flowType;
      this.eventFactory = eventFactory;
      this.oathTokenVerifier = oathTokenVerifier;
      this.helpdeskLogger = helpdeskLogger;
   }

   public Result<OathOtpActivationService.OathActivationData, OathOtpActivationService.ActivationError> createActivationData() {
      return this.loadCredBean(this.userId().getValue()).flatMap(this::prepareCredBean).flatMap(this::generateActivationData);
   }

   public TokenVerifierResponse verifyToken(String token) {
      CredentialBean credBean = this.loadCredBean(this.userId().getValue()).get();
      boolean isNewCredential = this.isNewCredential(credBean);
      credBean.getInitializedCurrent().setData(((OathOtpActivationStepSession)this.stepSession.get()).getCredBean().orElseThrow().serialize());

      try {
         TokenVerifierResponse verifierResponse = ((OathTokenVerifierDelegate)this.oathTokenVerifier.get())
            .checkToken(this.userId().getValue(), token, credBean);
         if (TokenVerifierResponse.TOKEN_ACCEPTED.equals(verifierResponse) && isNewCredential) {
            ((OathOtpPersistingService)this.persistingService.get()).persist(this.userId(), credBean);
         }

         return verifierResponse;
      } catch (PersisterException e) {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "Could not persist credential after successful validation", e);
      } catch (TokenVerifierException e) {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "Could not verify token", e);
      }

      return TokenVerifierResponse.UNKNOWN;
   }

   private boolean isNewCredential(CredentialBean credBean) {
      return Objects.isNull(credBean.getInitializedCurrent().getData()) || this.isOverwrite();
   }

   private Result<CredentialBean, OathOtpActivationService.ActivationError> prepareCredBean(CredentialBean credentialBean) {
      if (this.isNewCredential(credentialBean)) {
         Result<String, EncryptionException> result = OathOtpCredentialGenerator.generateCredData(((OathOtpSettings)this.oathOtpSettings.get()).getPassword());
         if (result.isFailure()) {
            LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "Cannot generate new credential data.", (Throwable)result.getFailure());
            return Result.failure(OathOtpActivationService.ActivationError.NOT_OATH_CREDENTIAL);
         }

         credentialBean.getInitializedCurrent().setData(result.get());
      } else {
         ((HelpdeskLogger)this.helpdeskLogger.get()).log(this.userId(), "An existing OATH shared secret has been viewed in order to register a device.");
         ((OathOtpDeviceActivatedEventFactory)this.eventFactory.get()).publishOathOtpSecretViewedEvent(this.userId());
      }

      return Result.result(credentialBean);
   }

   private Result<OathOtpActivationService.OathActivationData, OathOtpActivationService.ActivationError> generateActivationData(CredentialBean credBean) {
      OtpCredBeanData otp = OtpCredBeanData.deserialize(credBean.getInitializedCurrent().getStringData());

      try {
         String base32SharedSecret = OathOtpUtilities.encodeBase32ForGoogleAuthenticator(
            otp.decryptSharedSecret(((OathOtpSettings)this.oathOtpSettings.get()).getPassword())
         );
         String label = ((OathOtpSettings)this.oathOtpSettings.get()).getOrganizationLabel(credBean);
         String qrUrl = OathOtpUtilities.constructOathOtpQrUrl(this.userId().getValue(), label, base32SharedSecret, Optional.empty());
         ((OathOtpActivationStepSession)this.stepSession.get()).setCredBean(otp);
         return this.createQrCodeImage(qrUrl, base32SharedSecret);
      } catch (DecryptionException e) {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "Could not decrypt existing OATH OTP secret for user " + LogUtil.logSafeQuote(this.userId().getValue()));
         return Result.failure(OathOtpActivationService.ActivationError.UNSUPPORTED_OATH_CREDENTIAL);
      }
   }

   private Result<OathOtpActivationService.OathActivationData, OathOtpActivationService.ActivationError> createQrCodeImage(
      String qrUrl, String base32SharedSecret
   ) {
      try {
         BufferedImage qrImage = QrCodeUtilities.createQrCodeImage(qrUrl, 300, 300);
         return Result.result(new OathOtpActivationService.OathActivationData(base32SharedSecret, qrImage));
      } catch (ViolationException e) {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "Could not create QR Code for activation.", e);
         return Result.failure(OathOtpActivationService.ActivationError.UNSUPPORTED_OATH_CREDENTIAL);
      }
   }

   @VisibleForTesting
   Result<CredentialBean, OathOtpActivationService.ActivationError> loadCredBean(String username) throws UserEntityNotFoundException {
      if (KnownMultiFlowType.USER_SELF_REG.equals(this.flowType.get())) {
         return Result.result(new CredentialBeanImpl(CredentialBean.CredentialDataEncoding.STRING));
      }

      try {
         CredentialBean credBean = ((OathOtpSettings)this.oathOtpSettings.get()).getCredentialPersister().getPersistentCredentialByUsername(username);
         if (!credBean.isReallyActive()) {
            LOG.debug("OATH credential is not really active.");
            return this.isOverwrite() ? Result.result(credBean) : Result.failure(OathOtpActivationService.ActivationError.TOKEN_INACTIVE);
         } else {
            CredentialData initializedCurrent = credBean.getInitializedCurrent();
            if (initializedCurrent.getData() != null && initializedCurrent.getEncoding() != CredentialBean.CredentialDataEncoding.STRING) {
               return this.isOverwrite() ? Result.result(credBean) : Result.failure(OathOtpActivationService.ActivationError.NOT_OATH_CREDENTIAL);
            } else {
               return Result.result(credBean);
            }
         }
      } catch (NotFoundException e) {
         LOG.debug("User not found: " + username);
         return Result.failure(OathOtpActivationService.ActivationError.NO_USER);
      } catch (PersisterException e) {
         return Result.failure(OathOtpActivationService.ActivationError.PERSISTENCE);
      }
   }

   private UserId userId() {
      return ((PrincipalInfo)this.user.get()).getKnownIdentity().orElseThrow().id();
   }

   private boolean isOverwrite() {
      return ((OathOtpActivationStepSettings)this.settings.get()).overwriteExistingSecret();
   }

   public enum ActivationError {
      PERSISTENCE,
      TOKEN_INACTIVE,
      NO_USER,
      NOT_OATH_CREDENTIAL,
      UNSUPPORTED_OATH_CREDENTIAL;
   }

   public record OathActivationData(String sharedSecret, BufferedImage qrCodeImage) {
      public URI asEncodedQrCode() {
         return QrCodeUtilities.createPngDataUriFromImage(this.qrCodeImage);
      }
   }
}
