package com.airlock.iam.common.application.service.token.oathotp;

import com.airlock.iam.base.api.domain.model.exception.EntityNotFoundException;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.api.domain.model.user.UserId;
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.domain.model.token.oathotp.OathOtpToken;
import com.airlock.iam.core.misc.impl.tokenverifier.oathotp.OathOtpSettings;
import com.airlock.iam.core.misc.impl.tokenverifier.oathotp.OtpCredBeanData;
import com.airlock.iam.core.misc.persistency.CredentialBean;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.airlock.iam.security.util.crypto.DecryptionException;
import com.airlock.iam.security.util.crypto.EncryptionException;
import com.google.common.base.Preconditions;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import java.util.Optional;
import java.util.function.UnaryOperator;
import org.apache.commons.lang3.StringUtils;

public class CommonOathOtpTokenServiceImpl {
   private final OathOtpSettings oathOtpSettings;
   private final SigningLogger signingLogger;

   @Inject
   public CommonOathOtpTokenServiceImpl(OathOtpSettings oathOtpSettings, SigningLogger signingLogger) {
      this.oathOtpSettings = oathOtpSettings;
      this.signingLogger = signingLogger;
   }

   public OathOtpToken createToken(UserId userId) throws ViolationException {
      CredentialBean credBean = this.getCredentialBean(userId);
      if (!this.hasSecret(credBean)) {
         this.provideNewSharedSecret(userId, credBean);
         this.signingLogger.log("Created OATH OTP token for user \"%s\"", new Object[]{userId});
         return OathOtpToken.of(
            userId.getValue(),
            this.getDecryptedSharedSecret(credBean),
            this.getOrganizationLabel(userId).orElse(null),
            this.getCredBeanData(credBean).get().getCounter(),
            this.oathOtpSettings().isTimeBased()
         );
      } else {
         this.signingLogger.log("Cannot create OATH OTP token for user \"%s\" because this user already has one", new Object[]{userId});
         throw new ViolationException("cannot create OATH OTP token for user \"%s\" because this user already has one");
      }
   }

   public CredentialBean getCredentialBean(UserId userId) {
      try {
         return this.oathOtpSettings().getCredentialPersister().getPersistentCredentialByUsername(userId.getValue());
      } catch (NotFoundException e) {
         throw new EntityNotFoundException(e);
      } catch (PersisterException e) {
         throw new FailureException("Error reading credential data: " + e.getMessage(), e);
      }
   }

   public boolean hasSecret(CredentialBean credBean) {
      Optional<OtpCredBeanData> beanData = this.getCredBeanData(credBean);
      return beanData.isPresent() && StringUtils.isNotBlank(beanData.get().getSharedSecret());
   }

   public OathOtpSettings oathOtpSettings() {
      return this.oathOtpSettings;
   }

   private void provideNewSharedSecret(UserId userId, CredentialBean credBean) {
      Optional<OtpCredBeanData> beanData = this.getCredBeanData(credBean);

      try {
         if (beanData.isPresent()) {
            beanData.get().setNewSharedSecret(this.oathOtpSettings().getPassword());
         } else {
            beanData = Optional.of(new OtpCredBeanData(this.oathOtpSettings().getPassword()));
         }
      } catch (EncryptionException e) {
         throw new FailureException("Cannot encrypt OATH OTP credential data: " + e.getMessage(), e);
      }

      credBean.getInitializedCurrent().setData(beanData.get().serialize());
      this.makeCredentialPersistent(userId, credBean);
      this.signingLogger.log("Saved new shared secret for OATH OTP token for user \"%s\"", new Object[]{userId});
   }

   public Optional<byte[]> getDecryptedSharedSecret(CredentialBean credBean) {
      Optional<OtpCredBeanData> beanData = this.getCredBeanData(credBean);
      if (beanData.isPresent()) {
         try {
            return beanData.get().decryptSharedSecret(this.oathOtpSettings().getPassword());
         } catch (DecryptionException e) {
            throw new FailureException("Cannot decode OATH OTP credential data: " + e.getMessage(), e);
         }
      } else {
         return Optional.empty();
      }
   }

   public Optional<OtpCredBeanData> getCredBeanData(CredentialBean credBean) {
      if (credBean != null && credBean.hasCurrentData()) {
         try {
            return Optional.of(OtpCredBeanData.deserialize(credBean.getCurrent().getStringData()));
         } catch (IndexOutOfBoundsException | NumberFormatException e) {
            throw new FailureException("Cannot decode OATH OTP credential data: " + e.getMessage(), e);
         }
      } else {
         return Optional.empty();
      }
   }

   public Optional<String> getOrganizationLabel(UserId userId) {
      String label = this.oathOtpSettings().getOrganizationLabel(this.getCredentialBean(userId));
      return label != null ? Optional.of(label) : Optional.empty();
   }

   private void makeCredentialPersistent(UserId userId, CredentialBean credBean) {
      Preconditions.checkNotNull(credBean, "credential null");

      try {
         this.oathOtpSettings().getCredentialPersister().makeCredentialPersistent(userId.getValue(), credBean);
      } catch (PersisterException e) {
         throw new FailureException("Error storing credential for user \"" + userId + "\": " + e.getMessage(), e);
      }
   }

   public void updateChallenge(UserId userId, UnaryOperator<OtpCredBeanData> update) {
      if (this.oathOtpSettings().isSynchronizeIncreaseCounterButton()) {
         CredentialBean credentialBean = this.getCredentialBean(userId);
         Optional<OtpCredBeanData> beanDataOptional = this.getCredBeanData(credentialBean);
         if (beanDataOptional.isEmpty()) {
            throw new FailureException("Could not persist new challenge.");
         }

         OtpCredBeanData beanData = beanDataOptional.get();
         beanData = update.apply(beanData);
         credentialBean.getInitializedCurrent().setData(beanData.serialize());
         this.makeCredentialPersistent(userId, credentialBean);
         this.signingLogger.log("Saved new OATH OTP challenge for user \"%s\": %s.", new Object[]{userId, beanData.getCounter()});
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static CommonOathOtpTokenServiceImpl.CommonOathOtpTokenServiceImplBuilder builder() {
      return new CommonOathOtpTokenServiceImpl.CommonOathOtpTokenServiceImplBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class CommonOathOtpTokenServiceImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private OathOtpSettings oathOtpSettings;
      @SuppressFBWarnings(justification = "generated code")
      private SigningLogger signingLogger;

      @SuppressFBWarnings(justification = "generated code")
      CommonOathOtpTokenServiceImplBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public CommonOathOtpTokenServiceImpl.CommonOathOtpTokenServiceImplBuilder oathOtpSettings(OathOtpSettings oathOtpSettings) {
         this.oathOtpSettings = oathOtpSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CommonOathOtpTokenServiceImpl.CommonOathOtpTokenServiceImplBuilder signingLogger(SigningLogger signingLogger) {
         this.signingLogger = signingLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CommonOathOtpTokenServiceImpl build() {
         return new CommonOathOtpTokenServiceImpl(this.oathOtpSettings, this.signingLogger);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "CommonOathOtpTokenServiceImpl.CommonOathOtpTokenServiceImplBuilder(oathOtpSettings="
            + this.oathOtpSettings
            + ", signingLogger="
            + this.signingLogger
            + ")";
      }
   }
}
