package com.airlock.iam.publicselfservice.domain.model.step;

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.email.EmailAddress;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.time.TimeProvider;
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.domain.model.email.EmailAddressResolver;
import com.airlock.iam.common.domain.model.otp.OneTimePasswordFactory;
import com.airlock.iam.common.domain.model.principal.UserPrincipal;
import com.airlock.iam.common.domain.service.email.EmailSenderService;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactorDetail;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.publicselfservice.api.domain.model.PublicSelfServiceUser;
import com.airlock.iam.publicselfservice.application.configuration.steps.EmailIdentityVerificationStepConfig;
import com.airlock.iam.publicselfservice.domain.model.flow.session.OtpIdentityVerificationStepSession;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

@LogicForConfig(EmailIdentityVerificationStepConfig.class)
public final class EmailIdentityVerificationStepImpl implements EmailIdentityVerificationStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(EmailIdentityVerificationStepImpl.class);
   private final Provider<OtpIdentityVerificationStepSession> session;
   private final Provider<PublicSelfServiceUser> user;
   private final Provider<UserPrincipal> userPrincipal;
   private final Provider<Locale> language;
   private final Provider<OneTimePasswordFactory> otpFactory;
   private final EmailIdentityVerificationStepSettings settings;
   private final EmailSenderService emailSender;
   private final TimeProvider timeProvider;

   @Inject
   public EmailIdentityVerificationStepImpl(
      Provider<OtpIdentityVerificationStepSession> session,
      Provider<PublicSelfServiceUser> user,
      Provider<UserPrincipal> userPrincipal,
      Provider<Locale> language,
      Provider<OneTimePasswordFactory> otpFactory,
      EmailIdentityVerificationStepSettings settings,
      EmailSenderService emailSender,
      TimeProvider timeProvider
   ) {
      this.session = session;
      this.user = user;
      this.userPrincipal = userPrincipal;
      this.language = language;
      this.otpFactory = otpFactory;
      this.settings = settings;
      this.emailSender = emailSender;
      this.timeProvider = timeProvider;
   }

   @Override
   public Optional<StepSession> createSession() {
      return Optional.of(new OtpIdentityVerificationStepSession(this.settings.getMaxFailedAttempts()));
   }

   @Override
   public AuthenticationStepResult initialize() {
      if (!((PublicSelfServiceUser)this.user.get()).isPublicSelfServiceAllowed()) {
         LOG.info("Not sending email because user " + this.quotedName() + " doesn't exist or is not allowed to do public self-service.");
         return this.simulateEmailVerification();
      } else {
         Optional<EmailAddress> emailAddress = this.emailAddress();
         if (emailAddress.isEmpty()) {
            LOG.info("Not sending email because user " + this.quotedName() + " doesn't have a valid email address.");
            return this.simulateEmailVerification();
         } else {
            OneTimePassword otp = ((OneTimePasswordFactory)this.otpFactory.get()).createOneTimePassword();
            ((OtpIdentityVerificationStepSession)this.session.get()).setOtp(otp);
            this.emailSender.sendEmail(emailAddress.get(), (Locale)this.language.get(), this.collectVariables(otp.value()));
            LOG.info("Sent email with OTP to user " + this.quotedName());
            return this.stepResultFactory().stay(EmailIdentityVerificationStep.EmailOtpStepAction.EMAIL_OTP_REQUIRED).build();
         }
      }
   }

   private Optional<EmailAddress> emailAddress() {
      return EmailAddressResolver.resolveEmailAddress(
         this.settings.getEmailItem(), ((UserPrincipal)this.userPrincipal.get()).user().getContextData(), ((UserPrincipal)this.userPrincipal.get()).getName()
      );
   }

   private Map<String, Object> collectVariables(Otp otp) {
      Map<String, Object> variables = new HashMap<>(((UserPrincipal)this.userPrincipal.get()).user().getContextData());
      variables.put("USERNAME", ((PublicSelfServiceUser)this.user.get()).getIdentity().providedUsername().getValue());
      variables.put("TOKEN", otp.rawValue());
      variables.put("Now", this.timeProvider.getCurrentTime());
      return variables;
   }

   public AuthenticationStepResult verifyEmailOtp(UnverifiedOtp otp) {
      if (!((OtpIdentityVerificationStepSession)this.session.get()).hasOtp()) {
         return this.stepResultFactory().failDueToUnexpectedCall("no OTP").build();
      }

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

   private AuthenticationStepResult expiredOtp() {
      return this.stepResultFactory().fail(EmailIdentityVerificationStep.EmailOtpStepError.EMAIL_OTP_EXPIRED).build();
   }

   private AuthenticationStepResult incorrectOtp() {
      ((OtpIdentityVerificationStepSession)this.session.get()).increaseFailedAttempts();
      if (!((OtpIdentityVerificationStepSession)this.session.get()).isRetryPossible()) {
         LOG.info("User " + this.quotedName() + " failed to verify OTP, no further attempts left");
         return this.stepResultFactory().fail(EmailIdentityVerificationStep.EmailOtpStepError.EMAIL_OTP_WRONG).build();
      } else {
         LOG.info(
            "User "
               + this.quotedName()
               + " failed to verify OTP, remaining attempts: "
               + ((OtpIdentityVerificationStepSession)this.session.get()).getRemainingAttempts()
         );
         return this.stepResultFactory()
            .failRetry(EmailIdentityVerificationStep.EmailOtpStepError.EMAIL_OTP_WRONG, EmailIdentityVerificationStep.EmailOtpStepAction.EMAIL_OTP_REQUIRED)
            .build();
      }
   }

   private AuthenticationStepResult correctOtp() {
      if (((PublicSelfServiceUser)this.user.get()).isPublicSelfServiceAllowed()) {
         return this.stepResultFactory().success().build();
      }

      LOG.info("Email identity verification failed for user " + this.quotedName() + " because the public self-service is no longer allowed");
      return this.stepResultFactory().fail(EmailIdentityVerificationStep.EmailOtpStepError.EMAIL_OTP_WRONG).build();
   }

   private AuthenticationStepResult simulateEmailVerification() {
      ((OtpIdentityVerificationStepSession)this.session.get()).setOtp(((OneTimePasswordFactory)this.otpFactory.get()).createSimulatedOneTimePassword());
      return this.stepResultFactory().stay(EmailIdentityVerificationStep.EmailOtpStepAction.EMAIL_OTP_REQUIRED).build();
   }

   @Override
   public String getDescription() {
      return "Email Identity Verification Step";
   }

   private String quotedName() {
      return LogUtil.logSafeQuote(((PublicSelfServiceUser)this.user.get()).getIdentity().id());
   }

   @Override
   public AuthenticationMethod getAuthenticationMethod() {
      return this.settings.getAuthMethodId();
   }

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