package com.airlock.iam.login.rest.infrastructure.resource.prot;

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.base.misc.adapt.Adapter;
import com.airlock.iam.base.misc.date.LocalDateHelper;
import com.airlock.iam.common.api.domain.model.persistency.exception.PersisterException;
import com.airlock.iam.common.domain.model.auth.ExternalizationContext;
import com.airlock.iam.common.domain.service.externalization.ExternalizationService;
import com.airlock.iam.core.domain.model.cronto.challenge.crontoengine.CrontoEngineActivation2Challenge;
import com.airlock.iam.core.domain.model.cronto.challenge.vascocronto.SecureChannelMessage;
import com.airlock.iam.core.domain.model.cronto.challenge.vascocronto.VascoCrontoActivation2Challenge;
import com.airlock.iam.core.misc.cronto.CrontoDeviceInfo;
import com.airlock.iam.core.misc.impl.cronto.CrontoActivationSession;
import com.airlock.iam.core.misc.impl.cronto.crontoengine.CrontoEngineChallengeParameters;
import com.airlock.iam.core.misc.impl.cronto.crontoengine.CrontoEngineDeviceInfo;
import com.airlock.iam.core.misc.impl.cronto.crontoengine.CrontoEngineHandler;
import com.airlock.iam.core.misc.impl.cronto.crontoengine.CrontoEngineUtils;
import com.airlock.iam.core.misc.impl.cronto.vascocronto.VascoCrontoDeviceInfo;
import com.airlock.iam.core.misc.impl.cronto.vascocronto.VascoCrontoHandler;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoAccountToken;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoLetterToken;
import com.airlock.iam.core.misc.impl.token.cronto.crontoengine.CrontoEngineAccountToken;
import com.airlock.iam.core.misc.util.vasco.VascoActivation2Challenge;
import com.airlock.iam.login.rest.application.configuration.CrontoSelfServiceRestConfig;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.codec.binary.Base64;

final class CrontoActivationSessionExternalizer {
   private final CrontoSelfServiceRestConfig selfServiceConfig;
   private final ExternalizationService externalizationService;
   private static final String ACTIVATION_LETTER_ID = "activation-letter-id";
   private static final String CHALLENGE_MESSAGE = "challenge-message";
   private static final String CHALLENGE_SECURE_CHANNEL_BLOB_DATA = "challenge-secure-channel-blob-data";
   private static final String CHALLENGE_SECURE_CHANNEL_BLOB_SERIAL_APPL = "challenge-secure-channel-blob-serial-appl";
   private static final String CHALLENGE_SECURE_CHANNEL_BLOB_CRYPTO_APPL_INDEX = "challenge-secure-channel-blob-crypto-appl-index";
   private static final String CHALLENGE_OTP_BLOB = "challenge-otp-blob";
   private static final String CHALLENGE_SEQUENCE_NUMBER = "challenge-sequence-number";
   private static final String DEVICE_ID = "device-id";
   private static final String DEVICE_PLATFORM_CODE = "device-platform-code";
   private static final String DEVICE_INFO_SEQUENCE_NUMBER = "device-info-sequence-number";
   private static final String NONCE = "nonce";
   private static final String SERVER_DEVICE_ID = "server-device-id";

   Optional<ExternalizationContext> write(CrontoActivationSession session) {
      ExternalizationContext.ExternalizationContextContent.ExternalizationContextContentBuilder contentBuilder = ExternalizationContext.ExternalizationContextContent.builder();
      contentBuilder.userId(session.getUserId().getValue()).expirationDate(this.calculateSessionExpirationDate());
      this.serializeSession(session, contentBuilder);
      return this.externalizationService.create(contentBuilder.build());
   }

   CrontoActivationSession read(UserId userId, ExternalizationContext context) {
      CrontoAccountToken account = this.selfServiceConfig.getCrontoHandler().getAccountForUser(userId).orElseThrow();
      ExternalizationContext.ExternalizationContextContent content = this.createExternalizationContextContent(context);
      if (this.selfServiceConfig.getCrontoHandler() instanceof VascoCrontoHandler) {
         return this.deserializeVascoCrontoSession(userId, account, content);
      } else if (this.selfServiceConfig.getCrontoHandler() instanceof CrontoEngineHandler) {
         return this.deserializeCrontoEngineSession(userId, account, content);
      } else {
         throw new FailureException("Unsupported CrontoHandler");
      }
   }

   private void serializeSession(
      CrontoActivationSession session, ExternalizationContext.ExternalizationContextContent.ExternalizationContextContentBuilder contentBuilder
   ) {
      contentBuilder.value("activation-letter-id", session.getLetterId().orElseThrow());
      if (this.selfServiceConfig.getCrontoHandler() instanceof VascoCrontoHandler) {
         this.serializeVascoCrontoSession(session, contentBuilder);
      } else {
         if (!(this.selfServiceConfig.getCrontoHandler() instanceof CrontoEngineHandler)) {
            throw new FailureException("Unsupported CrontoHandler");
         }

         serializeCrontoEngineSession(session, contentBuilder);
      }
   }

   private void serializeVascoCrontoSession(
      CrontoActivationSession session, ExternalizationContext.ExternalizationContextContent.ExternalizationContextContentBuilder contentBuilder
   ) {
      VascoCrontoActivation2Challenge challenge = Adapter.adapt(session.getChallenge2().orElseThrow()).to(VascoCrontoActivation2Challenge.class);
      VascoCrontoDeviceInfo deviceInfo = (VascoCrontoDeviceInfo)session.getDeviceInfo();
      contentBuilder.value("device-id", deviceInfo.getDeviceIdDuringActivation());
      contentBuilder.value("device-platform-code", Integer.toString(session.getDeviceInfo().getPlatformCode()));
      contentBuilder.value("device-info-sequence-number", Integer.toString(((VascoCrontoDeviceInfo)session.getDeviceInfo()).getSequenceNum()));
      contentBuilder.value("challenge-message", challenge.getSecureChannelMessage().getMessage());
      contentBuilder.value("challenge-secure-channel-blob-data", Base64.encodeBase64String(deviceInfo.getSecureChannelBlob().getDpData()));
      contentBuilder.value("challenge-secure-channel-blob-serial-appl", deviceInfo.getSecureChannelBlob().getSerialAppl());
      contentBuilder.value("challenge-secure-channel-blob-crypto-appl-index", Integer.toString(deviceInfo.getSecureChannelBlob().getCryptoApplIndex()));
      if (deviceInfo.getOtpBlob().isPresent()) {
         contentBuilder.value("challenge-otp-blob", Base64.encodeBase64String(deviceInfo.getOtpBlob().get().getDpData()));
      }

      contentBuilder.value("challenge-sequence-number", Integer.toString(deviceInfo.getSequenceNum()));
   }

   private static void serializeCrontoEngineSession(
      CrontoActivationSession session, ExternalizationContext.ExternalizationContextContent.ExternalizationContextContentBuilder contentBuilder
   ) {
      CrontoEngineActivation2Challenge challenge = (CrontoEngineActivation2Challenge)session.getChallenge2().orElseThrow();
      CrontoEngineDeviceInfo deviceInfo = (CrontoEngineDeviceInfo)session.getDeviceInfo();
      contentBuilder.value("nonce", challenge.getNonce());
      contentBuilder.value("server-device-id", challenge.getServerDeviceId());
      contentBuilder.value("device-platform-code", Integer.toString(deviceInfo.getPlatformCode()));
   }

   private CrontoActivationSession deserializeVascoCrontoSession(
      UserId userId, CrontoAccountToken account, ExternalizationContext.ExternalizationContextContent content
   ) {
      Map<String, String> values = content.getValues();

      try {
         CrontoActivationSession session = CrontoActivationSession.startWithLetter(userId);
         Optional<CrontoLetterToken> letter = this.getLetterWithId(userId, values.get("activation-letter-id"));
         CrontoDeviceInfo deviceInfo = this.extractVascoCrontoDeviceInfo(values, account);
         session.challenge1Validated(letter, deviceInfo);
         VascoCrontoActivation2Challenge challenge2 = new VascoCrontoActivation2Challenge(
            SecureChannelMessage.secureChannelMessage(values.get("challenge-message"))
         );
         session.setChallenge2(challenge2);
         return session;
      } catch (PersisterException e) {
         throw new FailureException("Error while deserializing session", e);
      }
   }

   private CrontoDeviceInfo extractVascoCrontoDeviceInfo(Map<String, String> values, CrontoAccountToken account) {
      VascoCrontoDeviceInfo deviceInfo = new VascoCrontoDeviceInfo(
         values.get("device-id"), account.getAccountId().getId(), Integer.parseInt(values.get("device-platform-code"))
      );
      deviceInfo.setSequenceNum(Integer.parseInt(values.get("device-info-sequence-number")));
      deviceInfo.setSecureChannelBlob(
         new VascoActivation2Challenge.VascoBlob(
            Base64.decodeBase64(values.get("challenge-secure-channel-blob-data")),
            values.get("challenge-secure-channel-blob-serial-appl"),
            Integer.parseInt(values.get("challenge-secure-channel-blob-crypto-appl-index"))
         )
      );
      Optional<VascoActivation2Challenge.VascoBlob> otpBlob = Optional.ofNullable(values.get("challenge-otp-blob"))
         .map(blob -> new VascoActivation2Challenge.VascoBlob(Base64.decodeBase64(blob), null, 0));
      deviceInfo.setOtpBlob(otpBlob);
      return deviceInfo;
   }

   private CrontoActivationSession deserializeCrontoEngineSession(
      UserId userId, CrontoAccountToken account, ExternalizationContext.ExternalizationContextContent content
   ) {
      try {
         CrontoActivationSession session = CrontoActivationSession.startWithLetter(userId);
         Optional<CrontoLetterToken> letter = this.getLetterWithId(userId, content.getValues().get("activation-letter-id"));
         CrontoDeviceInfo deviceInfo = new CrontoEngineDeviceInfo(Integer.parseInt(content.getValues().get("device-platform-code")));
         session.challenge1Validated(letter, deviceInfo);
         CrontoEngineChallengeParameters parameters = this.extractCrontoEngineActivationResponseParams(userId, account, content);
         session.setChallenge2(CrontoEngineUtils.createChallengeFromParameters(parameters));
         return session;
      } catch (PersisterException e) {
         throw new FailureException("Error while deserializing session", e);
      }
   }

   private Optional<CrontoLetterToken> getLetterWithId(UserId userId, String id) throws PersisterException {
      return this.selfServiceConfig.getCrontoHandler().getAllLettersForUser(userId).stream().filter(l -> l.getId().equals(id)).findFirst();
   }

   private Date calculateSessionExpirationDate() {
      return LocalDateHelper.toDate(LocalDateTime.now().plusSeconds(this.selfServiceConfig.getActivationSessionLifetime()));
   }

   private ExternalizationContext.ExternalizationContextContent createExternalizationContextContent(ExternalizationContext context) {
      Optional<ExternalizationContext.ExternalizationContextContent> content = this.externalizationService.verify(context);
      if (content.isEmpty()) {
         throw new ViolationException("invalid ExternalizationContext");
      } else {
         return content.get();
      }
   }

   private CrontoEngineChallengeParameters extractCrontoEngineActivationResponseParams(
      UserId userId, CrontoAccountToken account, ExternalizationContext.ExternalizationContextContent content
   ) {
      CrontoEngineAccountToken crontoEngineAccount = (CrontoEngineAccountToken)account;
      return CrontoEngineChallengeParameters.builder()
         .type(CrontoEngineChallengeParameters.CrontoEngineChallengeType.ACTIVATION_2)
         .serviceCode(((CrontoEngineHandler)this.selfServiceConfig.getCrontoHandler()).getCrontoEngineUtils().serviceCodeIfNotDefault())
         .userId(userId)
         .userKey(crontoEngineAccount.getUserKey())
         .accountId(crontoEngineAccount.getAccountId())
         .nonce(content.getValues().get("nonce"))
         .serverDeviceId(content.getValues().get("server-device-id"))
         .build();
   }

   @SuppressFBWarnings(justification = "generated code")
   CrontoActivationSessionExternalizer(CrontoSelfServiceRestConfig selfServiceConfig, ExternalizationService externalizationService) {
      this.selfServiceConfig = selfServiceConfig;
      this.externalizationService = externalizationService;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class CrontoActivationSessionExternalizerBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private CrontoSelfServiceRestConfig selfServiceConfig;
      @SuppressFBWarnings(justification = "generated code")
      private ExternalizationService externalizationService;

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

      @SuppressFBWarnings(justification = "generated code")
      public CrontoActivationSessionExternalizer.CrontoActivationSessionExternalizerBuilder selfServiceConfig(CrontoSelfServiceRestConfig selfServiceConfig) {
         this.selfServiceConfig = selfServiceConfig;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoActivationSessionExternalizer.CrontoActivationSessionExternalizerBuilder externalizationService(
         ExternalizationService externalizationService
      ) {
         this.externalizationService = externalizationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoActivationSessionExternalizer build() {
         return new CrontoActivationSessionExternalizer(this.selfServiceConfig, this.externalizationService);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "CrontoActivationSessionExternalizer.CrontoActivationSessionExternalizerBuilder(selfServiceConfig="
            + this.selfServiceConfig
            + ", externalizationService="
            + this.externalizationService
            + ")";
      }
   }
}
