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

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.email.EmailAddress;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.otp.OneTimePassword;
import com.airlock.iam.common.api.domain.model.otp.Otp;
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.api.domain.model.valueprovider.ValueProvider;
import com.airlock.iam.common.api.domain.service.email.EmailService;
import com.airlock.iam.common.api.domain.service.email.EmailServiceException;
import com.airlock.iam.common.domain.model.masking.StringMasking;
import com.airlock.iam.common.domain.model.otp.OneTimePasswordFactory;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactorDetail;
import com.airlock.iam.core.misc.impl.authen.UsedEmailInfo;
import com.airlock.iam.flow.api.domain.model.session.FlowAttributeRepository;
import com.airlock.iam.flow.api.domain.model.step.GenericStepError;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.shared.api.domain.model.message.EmailMessageProvider;
import com.airlock.iam.flow.shared.application.configuration.emailotp.CommonEmailOtpCheckingStepConfig;
import com.airlock.iam.flow.shared.domain.model.attributes.AuthTokenIdFlowSessionAttribute;
import com.airlock.iam.flow.shared.domain.model.otp.GenericOtpCheckingStep;
import com.airlock.iam.flow.shared.domain.model.step.email.EmailOtpAuthenticationStepSession;
import com.airlock.iam.flow.shared.domain.model.step.email.EmailOtpAuthenticationStepSettings;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;

@LogicForConfig(CommonEmailOtpCheckingStepConfig.class)
public final class EmailOtpCheckingStepImpl implements EmailOtpCheckingStep {
   private static final String EMAIL_ADDRESS_ATTRIBUTE = "emailAddress";
   private final Provider<EmailOtpAuthenticationStepSession> stepSession;
   private final Provider<FlowAttributeRepository> flowAttributeRepository;
   private final Provider<PrincipalInfo> user;
   private final Provider<EmailService> emailService;
   private final Provider<EmailMessageProvider> messageProvider;
   private final Provider<ValueProvider<String>> recipientAddress;
   private final Provider<Optional<StringMasking>> masking;
   private final Provider<OneTimePasswordFactory> otpFactory;
   private final Provider<EmailOtpAuthenticationStepSettings> settings;
   private final Provider<AuthenticationMethod> authMethod;
   private final HelpdeskLogger logger;

   @Inject
   public EmailOtpCheckingStepImpl(
      Provider<EmailOtpAuthenticationStepSession> stepSession,
      Provider<FlowAttributeRepository> flowAttributeRepository,
      Provider<PrincipalInfo> user,
      Provider<EmailService> emailService,
      Provider<EmailMessageProvider> messageProvider,
      Provider<ValueProvider<String>> recipientAddress,
      Provider<Optional<StringMasking>> masking,
      Provider<OneTimePasswordFactory> otpFactory,
      Provider<EmailOtpAuthenticationStepSettings> settings,
      Provider<AuthenticationMethod> authMethod,
      HelpdeskLogger logger
   ) {
      this.stepSession = stepSession;
      this.flowAttributeRepository = flowAttributeRepository;
      this.user = user;
      this.emailService = emailService;
      this.messageProvider = messageProvider;
      this.recipientAddress = recipientAddress;
      this.masking = masking;
      this.otpFactory = otpFactory;
      this.settings = settings;
      this.authMethod = authMethod;
      this.logger = logger;
   }

   @Override
   public Optional<StepSession> createSession() {
      return Optional.of(EmailOtpAuthenticationStepSession.emptySession());
   }

   @Override
   public AuthenticationStepResult initialize() {
      Optional<EmailAddress> recipient = this.determineEmailAddress();
      if (recipient.isEmpty()) {
         return this.stepResultFactory().fail(GenericStepError.NO_VALID_TOKEN).build();
      }

      OneTimePassword otp = ((OneTimePasswordFactory)this.otpFactory.get()).createOneTimePassword();
      EmailOtpAuthenticationStepSession session = (EmailOtpAuthenticationStepSession)this.stepSession.get();
      session.initialize(recipient.get(), otp);
      this.sendEmail(recipient.get(), otp.value());
      this.logger.log(this.userId(), "Sent email to " + LogUtil.quote(this.mask(recipient.get())) + " with OTP");
      return this.addAdditionalAttributes(this.stepResultFactory().stay(EmailOtpCheckingStep.EmailOtpStepActions.EMAIL_OTP_CHECK_REQUIRED));
   }

   private Optional<EmailAddress> determineEmailAddress() {
      String value = (String)((ValueProvider)this.recipientAddress.get()).getValue();
      if (StringUtils.isBlank(value)) {
         this.logger.log(this.userId(), "Unable to send email with OTP since no email address is available.");
         return Optional.empty();
      }

      Optional<EmailAddress> address = EmailAddress.emailAddress(value);
      if (address.isEmpty()) {
         this.logger.log(this.userId(), "Unable to send email with OTP since the email address is not valid: " + value);
      }

      return address;
   }

   private void sendEmail(EmailAddress recipient, Otp otp) {
      String subject = ((EmailMessageProvider)this.messageProvider.get()).generateSubject();
      String body = ((EmailMessageProvider)this.messageProvider.get())
         .generateBody(Optional.of(otp), ((EmailOtpAuthenticationStepSettings)this.settings.get()).messageTemplateIsHtml());

      try {
         ((EmailService)this.emailService.get())
            .sendEmail(recipient, subject, body, ((EmailOtpAuthenticationStepSettings)this.settings.get()).messageTemplateIsHtml());
      } catch (EmailServiceException e) {
         throw new FailureException("Failed to send email", e);
      }
   }

   public AuthenticationStepResult checkOtp(UnverifiedOtp otp) {
      EmailOtpAuthenticationStepSession session = (EmailOtpAuthenticationStepSession)this.stepSession.get();

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

   private AuthenticationStepResult expiredOtp() {
      this.logger.log(this.userId(), "Email OTP has expired.");
      return this.stepResultFactory().fail(GenericOtpCheckingStep.GenericOtpStepErrors.OTP_EXPIRED).build();
   }

   private AuthenticationStepResult incorrectOtp(EmailOtpAuthenticationStepSession session) {
      session.increaseFailedAttempts();
      if (session.getFailedAttempts() > ((EmailOtpAuthenticationStepSettings)this.settings.get()).maxRetries()) {
         this.logger.log(this.userId(), "Failed to verify email OTP, no further attempts left");
         return this.stepResultFactory().fail(GenericOtpCheckingStep.GenericOtpStepErrors.OTP_WRONG).build();
      } else {
         this.logger.log(this.userId(), "Failed to verify email OTP");
         return this.addAdditionalAttributes(
            this.stepResultFactory()
               .failRetry(GenericOtpCheckingStep.GenericOtpStepErrors.OTP_WRONG, EmailOtpCheckingStep.EmailOtpStepActions.EMAIL_OTP_CHECK_REQUIRED)
         );
      }
   }

   private AuthenticationStepResult correctOtp() {
      this.logger.log(this.userId(), "Successfully verified email OTP");
      ((FlowAttributeRepository)this.flowAttributeRepository.get())
         .putOrMergeAttribute(this.getAuthTokenIdAsFlowAttribute(((EmailOtpAuthenticationStepSession)this.stepSession.get()).getEmailAddress().orElseThrow()));
      return this.stepResultFactory().success().build();
   }

   private AuthTokenIdFlowSessionAttribute getAuthTokenIdAsFlowAttribute(EmailAddress emailAddress) {
      return AuthTokenIdFlowSessionAttribute.authTokenIdFlowSessionAttribute(UsedEmailInfo.of(emailAddress).toString());
   }

   public AuthenticationStepResult resend() {
      EmailOtpAuthenticationStepSession session = (EmailOtpAuthenticationStepSession)this.stepSession.get();
      if (!this.isResendPossible()) {
         this.logger.log(this.userId(), "Resending of email OTP is not allowed");
         return this.addAdditionalAttributes(
            this.stepResultFactory()
               .failRetry(EmailOtpCheckingStep.EmailOtpStepErrors.EMAIL_OTP_RESEND_REFUSED, EmailOtpCheckingStep.EmailOtpStepActions.EMAIL_OTP_CHECK_REQUIRED)
               .isFailedFactor(false)
         );
      } else {
         this.logger.log(this.userId(), "Requested resending of email OTP");
         session.handleResend(session.getOtp().orElseThrow().renewValidity());
         this.sendEmail(session.getEmailAddress().orElseThrow(), session.getOtp().orElseThrow().value());
         return this.addAdditionalAttributes(this.stepResultFactory().stay(EmailOtpCheckingStep.EmailOtpStepActions.EMAIL_OTP_CHECK_REQUIRED));
      }
   }

   private AuthenticationStepResult addAdditionalAttributes(AuthenticationStepResultBuilder stepResult) {
      return stepResult.addAdditionalAttribute(
            "emailAddress", this.mask(((EmailOtpAuthenticationStepSession)this.stepSession.get()).getEmailAddress().orElseThrow())
         )
         .addAdditionalAttribute("resendPossible", this.isResendPossible())
         .build();
   }

   private boolean isResendPossible() {
      return ((EmailOtpAuthenticationStepSession)this.stepSession.get()).getResends() < ((EmailOtpAuthenticationStepSettings)this.settings.get()).maxResends();
   }

   private String mask(EmailAddress emailAddress) {
      String address = emailAddress.value();
      return ((Optional)this.masking.get()).<String>map(m -> m.mask(address)).orElse(address);
   }

   private UserId userId() {
      return ((PrincipalInfo)this.user.get()).getKnownIdentity().orElseThrow(() -> new FailureException("User identity must be known")).id();
   }

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

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

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