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

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.AuthenticationStepResultFactory;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.otp.UnverifiedOtp;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactorDetail;
import com.airlock.iam.core.misc.authen.result.AuthenticationFailed;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.shared.application.configuration.mtan.CommonMtanApprovalStepConfig;
import com.airlock.iam.flow.shared.application.configuration.mtan.MtanOtpCheckConfigSpec;
import com.airlock.iam.flow.shared.domain.service.mtan.MtanOtpCheckingService;
import com.airlock.iam.flow.shared.domain.service.mtan.MtanOtpCheckingServiceImpl;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@LogicForConfig(CommonMtanApprovalStepConfig.class)
public class MtanOtpCheckingStepImpl implements MtanOtpCheckingStep {
   private static final String NUMBER_CHOICES_ATTRIBUTE = "numberChoices";
   private static final String CHOICE_ID_KEY = "id";
   private static final String CHOICE_NUMBER_KEY = "number";
   private static final String CHOICE_LABEL_KEY = "label";
   private static final String CHOICE_DEFAULT_DEVICE_KEY = "defaultNumber";
   private final Provider<MtanOtpCheckingService> service;
   private final Provider<MtanOtpCheckConfigSpec> mtanAuthenticationConfig;
   private final Provider<MtanOtpCheckingSession> stepSession;
   private final Provider<AuthenticationMethod> authMethod;

   @Inject
   public MtanOtpCheckingStepImpl(
      Provider<MtanOtpCheckingService> service,
      Provider<MtanOtpCheckConfigSpec> mtanAuthenticationConfig,
      Provider<MtanOtpCheckingSession> stepSession,
      Provider<AuthenticationMethod> authMethod
   ) {
      this.service = service;
      this.mtanAuthenticationConfig = mtanAuthenticationConfig;
      this.stepSession = stepSession;
      this.authMethod = authMethod;
   }

   @Override
   public Optional<StepSession> createSession() {
      return Optional.of(
         MtanOtpCheckingSession.builder()
            .maxAttempts(((MtanOtpCheckConfigSpec)this.mtanAuthenticationConfig.get()).getMaxWrongOtpChecks() + 1)
            .maxRetransmissions(((MtanOtpCheckConfigSpec)this.mtanAuthenticationConfig.get()).getMaxOtpResends())
            .build()
      );
   }

   @Override
   public AuthenticationStepResult initialize() {
      if (!((MtanOtpCheckingService)this.service.get()).isApplicableForPrincipal()) {
         return this.stepResultFactory().fail(AuthenticationFlowStep.GeneralAuthenticationStepError.NO_VALID_TOKEN).isFailedFactor(false).build();
      }

      MtanOtpCheckingServiceImpl.NewMtanAuthentication newAuthentication = ((MtanOtpCheckingService)this.service.get()).startMtanAuthentication();
      return newAuthentication == MtanOtpCheckingServiceImpl.NewMtanAuthentication.SELECT_MTAN_TOKEN
         ? this.stepResultFactory()
            .stay(MtanOtpCheckingStep.MtanAuthStepActions.MTAN_TOKEN_CHOICE_REQUIRED)
            .addAdditionalAttribute("numberChoices", toAdditionalAttribute(((MtanOtpCheckingService)this.service.get()).getMtanChoices()))
            .build()
         : this.mtanOtpRequired();
   }

   public AuthenticationStepResult checkOtp(UnverifiedOtp otp) {
      if (!((MtanOtpCheckingSession)this.stepSession.get()).hasOtp()) {
         return this.stepResultFactory().failDueToUnexpectedCall("mTAN OTP check not possible, no OTP sent").build();
      } else {
         MtanOtpCheckingServiceImpl.CheckOtpResult result = ((MtanOtpCheckingService)this.service.get()).checkOtp(otp);
         if (result == MtanOtpCheckingServiceImpl.CheckOtpResult.OTP_WRONG_RETRY) {
            return this.mtanOtpRequired(MtanOtpCheckingStep.MtanAuthStepErrors.MTAN_OTP_WRONG);
         } else {
            return result == MtanOtpCheckingServiceImpl.CheckOtpResult.SUCCESS
               ? this.stepResultFactory().success().build()
               : this.stepResultFactory()
                  .fail(
                     result == MtanOtpCheckingServiceImpl.CheckOtpResult.OTP_EXPIRED
                        ? MtanOtpCheckingStep.MtanAuthStepErrors.MTAN_OTP_EXPIRED
                        : MtanOtpCheckingStep.MtanAuthStepErrors.MTAN_OTP_WRONG
                  )
                  .build();
         }
      }
   }

   public AuthenticationStepResult getNumberChoice() {
      if (((MtanOtpCheckingSession)this.stepSession.get()).hasOtp()) {
         return this.stepResultFactory().failDueToUnexpectedCall("mTAN number choice not possible, OTP already sent").build();
      }

      List<AuthenticationFailed.MtanChoiceToken> mtanChoices = ((MtanOtpCheckingService)this.service.get()).getMtanChoices();
      return this.stepResultFactory()
         .stay(MtanOtpCheckingStep.MtanAuthStepActions.MTAN_TOKEN_CHOICE_REQUIRED)
         .resultData(new MtanOtpCheckingStep.MtanNumberChoices(mtanChoices))
         .build();
   }

   public AuthenticationStepResult selectMtanToken(String mtanTokenId) {
      if (!((MtanOtpCheckingSession)this.stepSession.get()).getMtanChoices().isEmpty()
         && !((MtanOtpCheckingSession)this.stepSession.get()).getSelectedChoice().isPresent()) {
         boolean selectionSuccessful = ((MtanOtpCheckingService)this.service.get()).selectMtanToken(mtanTokenId);
         return selectionSuccessful
            ? this.mtanOtpRequired()
            : this.stepResultFactory()
               .failRetry(MtanOtpCheckingStep.MtanAuthStepErrors.MTAN_TOKEN_CHOICE_FAILED, MtanOtpCheckingStep.MtanAuthStepActions.MTAN_TOKEN_CHOICE_REQUIRED)
               .addAdditionalAttribute("numberChoices", toAdditionalAttribute(((MtanOtpCheckingService)this.service.get()).getMtanChoices()))
               .build();
      } else {
         return this.stepResultFactory().failDueToUnexpectedCall("mTAN number choice unexpected").build();
      }
   }

   public AuthenticationStepResult resendOtp() {
      boolean resendSuccessful = ((MtanOtpCheckingService)this.service.get()).resendOtp();
      return resendSuccessful ? this.mtanOtpRequired() : this.mtanOtpRequired(MtanOtpCheckingStep.MtanAuthStepErrors.MTAN_OTP_RESEND_REFUSED);
   }

   public AuthenticationStepResult resendOtpPossible() {
      return !((MtanOtpCheckingSession)this.stepSession.get()).hasOtp()
         ? this.stepResultFactory().failDueToUnexpectedCall("no OTP to resend").build()
         : this.stepResultFactory().stay(MtanOtpCheckingStep.MtanAuthStepActions.MTAN_OTP_REQUIRED).resultData(this.resendPossible()).build();
   }

   @Override
   public AuthenticationMethod getAuthenticationMethod() {
      return (AuthenticationMethod)this.authMethod.get();
   }

   @Override
   public String getDescription() {
      return "mTAN Authentication Step";
   }

   private AuthenticationStepResult mtanOtpRequired() {
      return this.stepResultFactory()
         .stay(MtanOtpCheckingStep.MtanAuthStepActions.MTAN_OTP_REQUIRED)
         .addAdditionalAttribute("resendPossible", ((MtanOtpCheckingService)this.service.get()).resendOtpPossible())
         .addAdditionalAttribute("phoneNumber", this.maskedPhoneNumber())
         .build();
   }

   private AuthenticationStepResult mtanOtpRequired(MtanOtpCheckingStep.MtanAuthStepErrors error) {
      return this.stepResultFactory()
         .failRetry(error, MtanOtpCheckingStep.MtanAuthStepActions.MTAN_OTP_REQUIRED)
         .addAdditionalAttribute("resendPossible", ((MtanOtpCheckingService)this.service.get()).resendOtpPossible())
         .addAdditionalAttribute("phoneNumber", this.maskedPhoneNumber())
         .build();
   }

   private static List<Map<String, Object>> toAdditionalAttribute(List<AuthenticationFailed.MtanChoiceToken> choices) {
      return choices.stream().map(MtanOtpCheckingStepImpl::toChoiceMap).toList();
   }

   private static Map<String, Object> toChoiceMap(AuthenticationFailed.MtanChoiceToken choice) {
      Map<String, Object> map = new HashMap<>();
      map.put("id", choice.getId());
      map.put("number", choice.getMaskedPhoneNumber());
      choice.getLabel().ifPresent(label -> map.put("label", label));
      map.put("defaultNumber", choice.isPreSelected());
      return map;
   }

   private String maskedPhoneNumber() {
      return ((MtanOtpCheckingSession)this.stepSession.get()).getSelectedChoice().orElseThrow().getMaskedPhoneNumber();
   }

   private MtanOtpCheckingStep.MtanOtpResendPossible resendPossible() {
      return MtanOtpCheckingStep.MtanOtpResendPossible.of(((MtanOtpCheckingService)this.service.get()).resendOtpPossible());
   }

   public AuthenticationStepResultFactory stepResultFactory() {
      return AuthenticationStepResultFactory.forFactorDetail(KnownAuthenticationFactorDetail.OTP_MTAN);
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class MtanOtpCheckingStepImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<MtanOtpCheckingService> service;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<MtanOtpCheckConfigSpec> mtanAuthenticationConfig;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<MtanOtpCheckingSession> stepSession;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AuthenticationMethod> authMethod;

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

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingStepImpl.MtanOtpCheckingStepImplBuilder service(Provider<MtanOtpCheckingService> service) {
         this.service = service;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingStepImpl.MtanOtpCheckingStepImplBuilder mtanAuthenticationConfig(Provider<MtanOtpCheckConfigSpec> mtanAuthenticationConfig) {
         this.mtanAuthenticationConfig = mtanAuthenticationConfig;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingStepImpl.MtanOtpCheckingStepImplBuilder stepSession(Provider<MtanOtpCheckingSession> stepSession) {
         this.stepSession = stepSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingStepImpl.MtanOtpCheckingStepImplBuilder authMethod(Provider<AuthenticationMethod> authMethod) {
         this.authMethod = authMethod;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingStepImpl build() {
         return new MtanOtpCheckingStepImpl(this.service, this.mtanAuthenticationConfig, this.stepSession, this.authMethod);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "MtanOtpCheckingStepImpl.MtanOtpCheckingStepImplBuilder(service="
            + this.service
            + ", mtanAuthenticationConfig="
            + this.mtanAuthenticationConfig
            + ", stepSession="
            + this.stepSession
            + ", authMethod="
            + this.authMethod
            + ")";
      }
   }
}
