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

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.common.api.domain.model.otp.OneTimePassword;
import com.airlock.iam.common.api.domain.model.otp.OtpEqualityStrategy;
import com.airlock.iam.common.api.domain.model.otp.UnverifiedOtp;
import com.airlock.iam.common.domain.model.otp.OneTimePasswordFactory;
import com.airlock.iam.common.domain.model.sms.Sms;
import com.airlock.iam.common.domain.model.sms.SmsMessageId;
import com.airlock.iam.common.domain.service.auth.ServiceResultCode;
import com.airlock.iam.common.domain.service.sms.MtanSmsFactory;
import com.airlock.iam.common.domain.service.sms.SmsService;
import com.airlock.iam.core.misc.authen.result.AuthenticationFailed;
import com.airlock.iam.core.misc.impl.authen.MtanUsedDeviceInfo;
import com.airlock.iam.core.misc.impl.token.MtanItem;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.session.FlowSession;
import com.airlock.iam.flow.shared.api.domain.model.message.MtanMessageProvider;
import com.airlock.iam.flow.shared.domain.model.attributes.AuthTokenIdAttribute;
import com.airlock.iam.flow.shared.domain.model.attributes.AuthTokenIdFlowSessionAttribute;
import com.airlock.iam.flow.shared.domain.model.mtan.MtanOtpCheckingSession;
import com.airlock.iam.flow.shared.domain.model.mtan.UserPrincipalWithMtan;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.List;
import java.util.Optional;

public final class MtanOtpCheckingServiceImpl implements MtanOtpCheckingService {
   private static final ELogger LOG = ELoggerFactory.getLogger(MtanOtpCheckingServiceImpl.class);
   private final MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings settings;
   private final Provider<MtanOtpCheckingSession> session;
   private final UserPrincipalWithMtan principal;
   private final Provider<OneTimePasswordFactory> otpFactory;
   private final MtanSmsFactory smsFactory;
   private final SmsService smsService;
   private final HelpdeskLogger helpdeskLogger;
   private final Optional<AuthTokenIdAttribute> authTokenIdAttribute;
   private final FlowSession flowSession;
   private final Provider<MtanMessageProvider> messageProvider;

   @Inject
   public MtanOtpCheckingServiceImpl(
      MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings settings,
      Provider<MtanOtpCheckingSession> session,
      UserPrincipalWithMtan principal,
      Provider<OneTimePasswordFactory> otpFactory,
      MtanSmsFactory smsFactory,
      SmsService smsService,
      HelpdeskLogger helpdeskLogger,
      Optional<AuthTokenIdAttribute> authTokenIdAttribute,
      FlowSession flowSession,
      Provider<MtanMessageProvider> messageProvider
   ) {
      this.settings = settings;
      this.session = session;
      this.principal = principal;
      this.otpFactory = otpFactory;
      this.smsFactory = smsFactory;
      this.smsService = smsService;
      this.helpdeskLogger = helpdeskLogger;
      this.authTokenIdAttribute = authTokenIdAttribute;
      this.flowSession = flowSession;
      this.messageProvider = messageProvider;
   }

   @Override
   public boolean isApplicableForPrincipal() {
      return !this.getMtanChoices().isEmpty();
   }

   @Override
   public MtanOtpCheckingServiceImpl.NewMtanAuthentication startMtanAuthentication() {
      ViolationException.checkTrue(this.isApplicableForPrincipal(), String.format("mTAN is not applicable for user '%s'", this.principal.getName()));
      ViolationException.checkNotPresent(
         ((MtanOtpCheckingSession)this.session.get()).getSelectedChoice(),
         String.format("Cannot start a new mTAN authentication for user '%s' since one is already ongoing", this.principal.getName())
      );
      Optional<AuthenticationFailed.MtanChoiceToken> mostRecentOrSingleChoice = this.getMostRecentOrSingleChoice();
      if (mostRecentOrSingleChoice.isEmpty()) {
         return MtanOtpCheckingServiceImpl.NewMtanAuthentication.SELECT_MTAN_TOKEN;
      }

      this.addMtanChoicesToSession();
      this.selectMtanToken(mostRecentOrSingleChoice.get().getId());
      return MtanOtpCheckingServiceImpl.NewMtanAuthentication.OTP_REQUIRED;
   }

   @Override
   public MtanOtpCheckingServiceImpl.CheckOtpResult checkOtp(UnverifiedOtp otp) {
      ViolationException.checkTrue(
         ((MtanOtpCheckingSession)this.session.get()).hasOtp(), String.format("No mTAN OTP in session for user '%s'", this.principal.getName())
      );

      return switch (((MtanOtpCheckingSession)this.session.get())
         .getOtp()
         .verify(otp, OtpEqualityStrategy.equalityStrategy(!this.settings.isOtpCaseSensitive()))) {
         case EXPIRED -> this.expiredOtp();
         case WRONG_VALUE -> this.incorrectOtp();
         case SUCCESS -> this.correctOtp();
      };
   }

   @Nonnull
   private MtanOtpCheckingServiceImpl.CheckOtpResult correctOtp() {
      this.log("mTAN OTP for user '%s' was correct", this.principal.getName());
      AuthenticationFailed.MtanChoiceToken selectedChoice = ((MtanOtpCheckingSession)this.session.get()).getSelectedChoice().orElseThrow();
      this.principal.markLastChoiceAsUsed(selectedChoice);
      this.flowSession.flowAttributes().putOrMergeAttribute(this.getAuthTokenIdAsFlowAttribute(selectedChoice));
      ((MtanOtpCheckingSession)this.session.get()).destroy();
      return MtanOtpCheckingServiceImpl.CheckOtpResult.SUCCESS;
   }

   @Nonnull
   private MtanOtpCheckingServiceImpl.CheckOtpResult incorrectOtp() {
      ((MtanOtpCheckingSession)this.session.get()).increaseAttempts();
      if (!((MtanOtpCheckingSession)this.session.get()).maxAttemptsExceeded()) {
         this.log("mTAN OTP for user '%s' was incorrect - allowing retry", this.principal.getName());
         return MtanOtpCheckingServiceImpl.CheckOtpResult.OTP_WRONG_RETRY;
      } else {
         this.log("mTAN OTP for user '%s' was incorrect - no retry allowed", this.principal.getName());
         return MtanOtpCheckingServiceImpl.CheckOtpResult.OTP_WRONG_NO_RETRY;
      }
   }

   @Nonnull
   private MtanOtpCheckingServiceImpl.CheckOtpResult expiredOtp() {
      this.log("mTAN OTP for user '%s' has expired", this.principal.getName());
      return MtanOtpCheckingServiceImpl.CheckOtpResult.OTP_EXPIRED;
   }

   @Override
   public boolean resendOtp() {
      ViolationException.checkTrue(
         ((MtanOtpCheckingSession)this.session.get()).hasOtp(), String.format("No mTAN OTP in session for user '%s'", this.principal.getName())
      );
      if (((MtanOtpCheckingSession)this.session.get()).maxRetransmissionsExceeded()) {
         this.log("Not resending mTAN OTP user '%s' because max number of retransmissions reached", this.principal.getName());
         return false;
      } else {
         ((MtanOtpCheckingSession)this.session.get()).increaseRetransmissions();
         OneTimePassword previousOtp = ((MtanOtpCheckingSession)this.session.get()).getOtp();
         ((MtanOtpCheckingSession)this.session.get()).clearOtp();
         ((MtanOtpCheckingSession)this.session.get()).setOtp(this.renewOtp(previousOtp));
         this.sendSms();
         return true;
      }
   }

   private OneTimePassword renewOtp(OneTimePassword currentOtp) {
      if (this.settings.isRetransmitSameToken()) {
         return currentOtp.renewValidity();
      }

      LOG.info("Generated new OTP for resend");
      return this.createOtp();
   }

   @Override
   public boolean resendOtpPossible() {
      ViolationException.checkTrue(
         ((MtanOtpCheckingSession)this.session.get()).hasOtp(), String.format("No mTAN OTP in session for user '%s'", this.principal.getName())
      );
      return !((MtanOtpCheckingSession)this.session.get()).maxRetransmissionsExceeded();
   }

   @Override
   public boolean selectMtanToken(String id) {
      ViolationException.checkTrue(
         ((MtanOtpCheckingSession)this.session.get()).getSelectedChoice().isEmpty(),
         String.format("Cannot select another mTAN token while a selection was already made for user '%s'", this.principal.getName())
      );
      Optional<AuthenticationFailed.MtanChoiceToken> choice = ((MtanOtpCheckingSession)this.session.get()).getMtanChoiceById(id);
      if (choice.isEmpty()) {
         LOG.debug(String.format("No mTAN choice with ID '%s' found in the mTAN session ", id));
         return false;
      } else {
         ((MtanOtpCheckingSession)this.session.get()).selectMtanChoice(choice.get());
         this.log("Selected mTAN choice with ID '%s'", id);
         ((MtanOtpCheckingSession)this.session.get()).setOtp(this.createOtp());
         this.sendSms();
         return true;
      }
   }

   @Override
   public List<AuthenticationFailed.MtanChoiceToken> getMtanChoices() {
      if (((MtanOtpCheckingSession)this.session.get()).getMtanChoices().isEmpty()) {
         this.addMtanChoicesToSession();
      }

      return ((MtanOtpCheckingSession)this.session.get()).getMtanChoices();
   }

   private void addMtanChoicesToSession() {
      ((MtanOtpCheckingSession)this.session.get()).setMtanChoices(this.principal.getMtanTokenChoices());
   }

   private OneTimePassword createOtp() {
      this.log("Generating new mTAN OTP for user '%s'", this.principal.getName());
      return ((OneTimePasswordFactory)this.otpFactory.get()).createOneTimePassword();
   }

   private void sendSms() {
      AuthenticationFailed.MtanChoiceToken choice = ViolationException.checkPresent(
         ((MtanOtpCheckingSession)this.session.get()).getSelectedChoice(), String.format("No mTAN number selected for user '%s'", this.principal.getName())
      );
      MtanItem mtan = ViolationException.checkPresent(
         this.principal.getMtanTokenById(choice.getId()),
         String.format("No active mTAN token with ID '%s' found for user '%s'", choice.getId(), this.principal.getName())
      );
      this.log("Sending mTAN OTP for user '%s' to mobile number '%s'", this.principal.getName(), choice.getMaskedPhoneNumber());
      Sms sms = this.smsFactory
         .createSmsFor(
            ((MtanOtpCheckingSession)this.session.get()).getOtp().value(), mtan, ((MtanMessageProvider)this.messageProvider.get()).generateMtanMessage()
         );
      SmsMessageId messageId = this.smsService.send(sms);
      LOG.info(String.format("Sent SMS with message id \"%s\" to user \"%s\"", messageId.exists() ? messageId.getId() : "<unknown>", this.principal.getName()));
   }

   private void log(String message, Object... args) {
      this.helpdeskLogger.log(this.principal.getName(), message, args);
   }

   private Optional<AuthenticationFailed.MtanChoiceToken> getMostRecentOrSingleChoice() {
      if (this.principal.getMtanTokenChoices().size() == 1 && this.settings.isAutoSelectSingleNumber()) {
         return Optional.of(this.principal.getMtanTokenChoices().getFirst());
      }

      if (this.authTokenIdAttribute.isPresent()) {
         Optional<AuthenticationFailed.MtanChoiceToken> token = this.getTokenFromAuthTokenId();
         if (token.isPresent()) {
            return token;
         }
      } else if (this.settings.isChooseLastNumberAutomatically()) {
         return this.principal.getMostRecentChoice();
      }

      return Optional.empty();
   }

   private Optional<AuthenticationFailed.MtanChoiceToken> getTokenFromAuthTokenId() {
      Optional<MtanUsedDeviceInfo> id = MtanUsedDeviceInfo.parse(this.authTokenIdAttribute.orElseThrow().getAuthTokenId());
      return id.isPresent() ? ((MtanOtpCheckingSession)this.session.get()).getMtanChoiceById(id.get().getDeviceId()) : Optional.empty();
   }

   private AuthTokenIdFlowSessionAttribute getAuthTokenIdAsFlowAttribute(AuthenticationFailed.MtanChoiceToken selectedChoice) {
      return AuthTokenIdFlowSessionAttribute.authTokenIdFlowSessionAttribute(MtanUsedDeviceInfo.of(selectedChoice.getId()).toString());
   }

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

   public enum CheckOtpResult implements ServiceResultCode {
      SUCCESS,
      OTP_EXPIRED,
      OTP_WRONG_RETRY,
      OTP_WRONG_NO_RETRY;
   }

   public static final class MtanAuthenticationServiceSettings {
      private final boolean retransmitSameToken;
      private final boolean chooseLastNumberAutomatically;
      private final boolean otpCaseSensitive;
      private final boolean autoSelectSingleNumber;

      @SuppressFBWarnings(justification = "generated code")
      MtanAuthenticationServiceSettings(
         boolean retransmitSameToken, boolean chooseLastNumberAutomatically, boolean otpCaseSensitive, boolean autoSelectSingleNumber
      ) {
         this.retransmitSameToken = retransmitSameToken;
         this.chooseLastNumberAutomatically = chooseLastNumberAutomatically;
         this.otpCaseSensitive = otpCaseSensitive;
         this.autoSelectSingleNumber = autoSelectSingleNumber;
      }

      @SuppressFBWarnings(justification = "generated code")
      public static MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings.MtanAuthenticationServiceSettingsBuilder builder() {
         return new MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings.MtanAuthenticationServiceSettingsBuilder();
      }

      @SuppressFBWarnings(justification = "generated code")
      public boolean isRetransmitSameToken() {
         return this.retransmitSameToken;
      }

      @SuppressFBWarnings(justification = "generated code")
      public boolean isChooseLastNumberAutomatically() {
         return this.chooseLastNumberAutomatically;
      }

      @SuppressFBWarnings(justification = "generated code")
      public boolean isOtpCaseSensitive() {
         return this.otpCaseSensitive;
      }

      @SuppressFBWarnings(justification = "generated code")
      public boolean isAutoSelectSingleNumber() {
         return this.autoSelectSingleNumber;
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public boolean equals(Object o) {
         if (o == this) {
            return true;
         } else if (!(o instanceof MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings other)) {
            return false;
         } else if (this.retransmitSameToken != other.retransmitSameToken) {
            return false;
         } else if (this.chooseLastNumberAutomatically != other.chooseLastNumberAutomatically) {
            return false;
         } else {
            return this.otpCaseSensitive != other.otpCaseSensitive ? false : this.autoSelectSingleNumber == other.autoSelectSingleNumber;
         }
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public int hashCode() {
         int PRIME = 59;
         int result = 1;
         result = result * 59 + (this.retransmitSameToken ? 79 : 97);
         result = result * 59 + (this.chooseLastNumberAutomatically ? 79 : 97);
         result = result * 59 + (this.otpCaseSensitive ? 79 : 97);
         return result * 59 + (this.autoSelectSingleNumber ? 79 : 97);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings(retransmitSameToken="
            + this.isRetransmitSameToken()
            + ", chooseLastNumberAutomatically="
            + this.isChooseLastNumberAutomatically()
            + ", otpCaseSensitive="
            + this.isOtpCaseSensitive()
            + ", autoSelectSingleNumber="
            + this.isAutoSelectSingleNumber()
            + ")";
      }

      @SuppressFBWarnings(justification = "generated code")
      public static class MtanAuthenticationServiceSettingsBuilder {
         @SuppressFBWarnings(justification = "generated code")
         private boolean retransmitSameToken;
         @SuppressFBWarnings(justification = "generated code")
         private boolean chooseLastNumberAutomatically;
         @SuppressFBWarnings(justification = "generated code")
         private boolean otpCaseSensitive;
         @SuppressFBWarnings(justification = "generated code")
         private boolean autoSelectSingleNumber;

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

         @SuppressFBWarnings(justification = "generated code")
         public MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings.MtanAuthenticationServiceSettingsBuilder retransmitSameToken(
            boolean retransmitSameToken
         ) {
            this.retransmitSameToken = retransmitSameToken;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings.MtanAuthenticationServiceSettingsBuilder chooseLastNumberAutomatically(
            boolean chooseLastNumberAutomatically
         ) {
            this.chooseLastNumberAutomatically = chooseLastNumberAutomatically;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings.MtanAuthenticationServiceSettingsBuilder otpCaseSensitive(boolean otpCaseSensitive) {
            this.otpCaseSensitive = otpCaseSensitive;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings.MtanAuthenticationServiceSettingsBuilder autoSelectSingleNumber(
            boolean autoSelectSingleNumber
         ) {
            this.autoSelectSingleNumber = autoSelectSingleNumber;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings build() {
            return new MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings(
               this.retransmitSameToken, this.chooseLastNumberAutomatically, this.otpCaseSensitive, this.autoSelectSingleNumber
            );
         }

         @SuppressFBWarnings(justification = "generated code")
         @Override
         public String toString() {
            return "MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings.MtanAuthenticationServiceSettingsBuilder(retransmitSameToken="
               + this.retransmitSameToken
               + ", chooseLastNumberAutomatically="
               + this.chooseLastNumberAutomatically
               + ", otpCaseSensitive="
               + this.otpCaseSensitive
               + ", autoSelectSingleNumber="
               + this.autoSelectSingleNumber
               + ")";
         }
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class MtanOtpCheckingServiceImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings settings;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<MtanOtpCheckingSession> session;
      @SuppressFBWarnings(justification = "generated code")
      private UserPrincipalWithMtan principal;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OneTimePasswordFactory> otpFactory;
      @SuppressFBWarnings(justification = "generated code")
      private MtanSmsFactory smsFactory;
      @SuppressFBWarnings(justification = "generated code")
      private SmsService smsService;
      @SuppressFBWarnings(justification = "generated code")
      private HelpdeskLogger helpdeskLogger;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<AuthTokenIdAttribute> authTokenIdAttribute;
      @SuppressFBWarnings(justification = "generated code")
      private FlowSession flowSession;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<MtanMessageProvider> messageProvider;

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

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder settings(MtanOtpCheckingServiceImpl.MtanAuthenticationServiceSettings settings) {
         this.settings = settings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder session(Provider<MtanOtpCheckingSession> session) {
         this.session = session;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder principal(UserPrincipalWithMtan principal) {
         this.principal = principal;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder otpFactory(Provider<OneTimePasswordFactory> otpFactory) {
         this.otpFactory = otpFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder smsFactory(MtanSmsFactory smsFactory) {
         this.smsFactory = smsFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder smsService(SmsService smsService) {
         this.smsService = smsService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder helpdeskLogger(HelpdeskLogger helpdeskLogger) {
         this.helpdeskLogger = helpdeskLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder authTokenIdAttribute(Optional<AuthTokenIdAttribute> authTokenIdAttribute) {
         this.authTokenIdAttribute = authTokenIdAttribute;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder flowSession(FlowSession flowSession) {
         this.flowSession = flowSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder messageProvider(Provider<MtanMessageProvider> messageProvider) {
         this.messageProvider = messageProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public MtanOtpCheckingServiceImpl build() {
         return new MtanOtpCheckingServiceImpl(
            this.settings,
            this.session,
            this.principal,
            this.otpFactory,
            this.smsFactory,
            this.smsService,
            this.helpdeskLogger,
            this.authTokenIdAttribute,
            this.flowSession,
            this.messageProvider
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "MtanOtpCheckingServiceImpl.MtanOtpCheckingServiceImplBuilder(settings="
            + this.settings
            + ", session="
            + this.session
            + ", principal="
            + this.principal
            + ", otpFactory="
            + this.otpFactory
            + ", smsFactory="
            + this.smsFactory
            + ", smsService="
            + this.smsService
            + ", helpdeskLogger="
            + this.helpdeskLogger
            + ", authTokenIdAttribute="
            + this.authTokenIdAttribute
            + ", flowSession="
            + this.flowSession
            + ", messageProvider="
            + this.messageProvider
            + ")";
      }
   }

   public enum NewMtanAuthentication implements ServiceResultCode {
      SELECT_MTAN_TOKEN,
      OTP_REQUIRED;
   }
}
