package com.airlock.iam.flow.shared.domain.model.cronto;

import com.airlock.iam.authentication.api.domain.model.step.AuthenticationFlowStep;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResult;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResultBuilder;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResultFactory;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.core.misc.cronto.CrontoChallenge;
import com.airlock.iam.core.misc.cronto.CrontoHandler;
import com.airlock.iam.core.misc.impl.authen.cronto.CrontoPushDeviceChoice;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoDeviceToken;
import com.airlock.iam.flow.shared.domain.service.cronto.CrontoCheckingService;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public final class CrontoAuthenticationResultMapper {
   private static final String CHALLENGE_ATTRIBUTE = "challenge";
   private static final String DEVICE_CHOICES_ATTRIBUTE = "deviceChoices";
   private static final String CHOICE_ID_KEY = "id";
   private static final String CHOICE_LABEL_KEY = "label";
   private static final String CHOICE_DEFAULT_DEVICE_KEY = "defaultDevice";

   public static AuthenticationStepResult map(
      CrontoCheckingService.CrontoCheckResult result, CrontoAuthChallenge challenge, AuthenticationStepResultFactory stepResultFactory
   ) {
      if (result.isSuccess()) {
         return stepResultFactory.success().build();
      }

      CrontoCheckingService.CheckFailureCode failure = result.getFailure().orElseThrow();

      return switch (failure) {
         case PENDING -> nextPendingResult(challenge, stepResultFactory).build();
         case WRONG_RETRY -> stepResultFactory.failRetry(
               CrontoCheckingStep.CrontoAuthStepErrors.CRONTO_OTP_WRONG, CrontoCheckingStep.CrontoAuthStepActions.CRONTO_OTP_REQUIRED
            )
            .addAdditionalAttribute("challenge", challenge)
            .build();
         case WRONG_NO_RETRY -> stepResultFactory.fail(CrontoCheckingStep.CrontoAuthStepErrors.CRONTO_OTP_WRONG).build();
         case DEVICE_BLOCKED -> stepResultFactory.fail(AuthenticationFlowStep.GeneralAuthenticationStepError.NO_VALID_TOKEN).build();
         case CANCELED -> stepResultFactory.fail(CrontoCheckingStep.CrontoAuthStepErrors.CANCELLED_BY_USER).build();
      };
   }

   public static AuthenticationStepResultBuilder nextPendingResult(CrontoAuthChallenge challenge, AuthenticationStepResultFactory stepResultFactory) {
      return stepResultFactory.stay(CrontoCheckingStep.CrontoAuthStepActions.CRONTO_OTP_REQUIRED).addAdditionalAttribute("challenge", challenge);
   }

   public static AuthenticationStepResult map(CrontoCheckingService.CrontoInitCheckResult initResult, AuthenticationStepResultFactory stepResultFactory) {
      if (initResult.getChallenge().isPresent()) {
         return crontoOtpRequired(initResult.getChallenge().orElseThrow(), stepResultFactory);
      } else if (!initResult.getDeviceChoices().isEmpty()) {
         return stepResultFactory.stay(CrontoCheckingStep.CrontoAuthStepActions.CRONTO_DEVICE_CHOICE_REQUIRED)
            .addAdditionalAttribute("deviceChoices", toAdditionalAttribute(initResult.getDeviceChoices()))
            .build();
      } else if (initResult.getFailure().isPresent()) {
         return stepResultFactory.fail(AuthenticationFlowStep.GeneralAuthenticationStepError.NO_VALID_TOKEN).build();
      } else {
         throw new FailureException("Invalid auth init result");
      }
   }

   public static AuthenticationStepResult map(CrontoCheckingService.PushDeviceSelectionResult result, AuthenticationStepResultFactory stepResultFactory) {
      if (result.getChallenge().isPresent()) {
         return crontoOtpRequired(result.getChallenge().orElseThrow(), stepResultFactory);
      } else if (!result.getDeviceChoices().isEmpty()) {
         return stepResultFactory.failRetry(
               CrontoCheckingStep.CrontoAuthStepErrors.CRONTO_DEVICE_CHOICE_FAILED, CrontoCheckingStep.CrontoAuthStepActions.CRONTO_DEVICE_CHOICE_REQUIRED
            )
            .addAdditionalAttribute("deviceChoices", toAdditionalAttribute(result.getDeviceChoices()))
            .build();
      } else {
         throw new FailureException("Invalid push device selection result");
      }
   }

   private static AuthenticationStepResult crontoOtpRequired(
      CrontoCheckingService.CrontoServiceChallenge challenge, AuthenticationStepResultFactory stepResultFactory
   ) {
      return stepResultFactory.stay(CrontoCheckingStep.CrontoAuthStepActions.CRONTO_OTP_REQUIRED)
         .addAdditionalAttribute("challenge", mapChallenge(challenge))
         .build();
   }

   public static CrontoAuthChallenge mapChallenge(CrontoCheckingService.CrontoServiceChallenge challenge) {
      CrontoChallenge crontoChallenge = challenge.getChallenge();
      return CrontoAuthChallenge.builder()
         .base64Cryptogram(crontoChallenge.getCryptogramForScreen().base64image())
         .secureChannelChallenge(crontoChallenge.getRawChallengeString())
         .onlineValidation(crontoChallenge.isOnlineValidation())
         .pushed(challenge.isPushChallenge())
         .pushLoginId(challenge.getPushLoginId())
         .pushDevices(challenge.getPushDevices().stream().map(CrontoAuthenticationResultMapper::mapDeviceToken).toList())
         .build();
   }

   private static CrontoPushDevice mapDeviceToken(CrontoDeviceToken crontoDeviceToken) {
      return new CrontoPushDevice(
         crontoDeviceToken.getDeviceIdForDisplay(),
         Optional.ofNullable(crontoDeviceToken.getCustomName()),
         crontoDeviceToken.getPlatformType().map(CrontoAuthenticationResultMapper::mapPlatformType)
      );
   }

   private static String mapPlatformType(CrontoHandler.PlatformType platformType) {
      return switch (platformType) {
         case WINDOWS, BLACKBERRY, IOS, ANDROID -> platformType.name();
         case ROOTED_IOS -> CrontoHandler.PlatformType.IOS.name();
         case ROOTED_ANDROID -> CrontoHandler.PlatformType.ANDROID.name();
         case STANDALONE -> throw new FailureException("Unexpected device platform " + platformType);
      };
   }

   private static List<Map<String, Object>> toAdditionalAttribute(List<CrontoPushDeviceChoice> choices) {
      return choices.stream().map(choice -> Map.of("id", choice.getDeviceId(), "label", choice.getLabel(), "defaultDevice", choice.isPreSelected())).toList();
   }

   private CrontoAuthenticationResultMapper() {
   }
}
