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

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.exception.FailureException;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
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.OtpEqualityStrategy;
import com.airlock.iam.common.api.domain.model.otp.UnverifiedOtp;
import com.airlock.iam.common.api.domain.model.user.User;
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.common.domain.model.user.UserIdentityLogs;
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.flow.shared.api.domain.model.message.EmailMessageProvider;
import com.airlock.iam.flow.shared.api.domain.model.validation.UniquenessValidationService;
import com.airlock.iam.flow.shared.application.configuration.step.email.EmailVerificationStepConfigBase;
import com.airlock.iam.flow.shared.domain.model.step.email.EmailVerificationStepSession;
import com.airlock.iam.flow.shared.domain.model.step.email.EmailVerificationStepSettings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import java.util.List;
import java.util.Optional;

@LogicForConfig(EmailVerificationStepConfigBase.class)
public class EmailVerificationStepImpl implements EmailVerificationStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(EmailVerificationStepImpl.class);
   private final Provider<EmailVerificationStepSession> session;
   private final Provider<User> user;
   private final Provider<PrincipalInfo> principalInfo;
   private final Provider<UniquenessValidationService> uniquenessValidationService;
   private final Provider<EmailMessageProvider> messageProvider;
   private final Provider<Optional<StringMasking>> masking;
   private final Provider<OneTimePasswordFactory> otpFactory;
   private final Optional<AuthenticationMethod> authMethod;
   private final EmailService emailService;
   private final EmailVerificationStepSettings settings;

   @Inject
   public EmailVerificationStepImpl(
      Provider<EmailVerificationStepSession> session,
      Provider<User> user,
      Provider<PrincipalInfo> principalInfo,
      Provider<UniquenessValidationService> uniquenessValidationService,
      Provider<EmailMessageProvider> messageProvider,
      Provider<Optional<StringMasking>> masking,
      Provider<OneTimePasswordFactory> otpFactory,
      Optional<AuthenticationMethod> authMethod,
      EmailService emailService,
      EmailVerificationStepSettings settings
   ) {
      this.session = session;
      this.user = user;
      this.principalInfo = principalInfo;
      this.uniquenessValidationService = uniquenessValidationService;
      this.messageProvider = messageProvider;
      this.masking = masking;
      this.otpFactory = otpFactory;
      this.authMethod = authMethod;
      this.emailService = emailService;
      this.settings = settings;
   }

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

   @Override
   public AuthenticationStepResult initialize() {
      ValidationResult validationResult = ((UniquenessValidationService)this.uniquenessValidationService.get())
         .validateUniqueness((User)this.user.get(), UniquenessValidationService.ValidationMode.REPORT_ALL);
      if (validationResult.isValid()) {
         OneTimePassword otp = ((OneTimePasswordFactory)this.otpFactory.get()).createOneTimePassword();
         ((EmailVerificationStepSession)this.session.get()).setOtp(otp);
         this.sendEmail(otp);
         LOG.info("Sent email with OTP to user " + this.quotedName());
         return this.emailOtpRequired();
      } else if (this.hasOnlyConflictsThatAreHiddenByStealthMode()) {
         OneTimePassword otp = ((OneTimePasswordFactory)this.otpFactory.get()).createSimulatedOneTimePassword();
         ((EmailVerificationStepSession)this.session.get()).setOtp(otp);
         LOG.info("Stealth Mode simulation: Channel verification email is not sent because a user with this email address already exists.");
         return this.emailOtpRequired();
      } else {
         throw new FailureException(
            String.format(
               "Channel verification of self-registering user %s aborted due to uniqueness constraint violation: %s", this.quotedName(), validationResult
            )
         );
      }
   }

   private AuthenticationStepResult emailOtpRequired() {
      return this.stepResultFactory()
         .stay(EmailVerificationStep.EmailVerificationStepAction.EMAIL_OTP_REQUIRED)
         .addAdditionalAttribute("emailAddress", this.maskedEmailAddress())
         .build();
   }

   private void sendEmail(OneTimePassword otp) {
      try {
         this.emailService
            .sendEmail(
               this.determineToAddress(),
               List.of(),
               List.of(),
               ((EmailMessageProvider)this.messageProvider.get()).generateSubject(),
               ((EmailMessageProvider)this.messageProvider.get()).generateBody(Optional.of(otp.value()), this.settings.isSendAsHtml()),
               this.settings.isSendAsHtml()
            );
      } catch (EmailServiceException e) {
         throw new FailureException(String.format("Error while sending verification email for user %s", this.quotedName()), e);
      }
   }

   private boolean hasOnlyConflictsThatAreHiddenByStealthMode() {
      return this.settings.isStealthModeEnabled()
         && ((UniquenessValidationService)this.uniquenessValidationService.get())
            .validateUniqueness((User)this.user.get(), UniquenessValidationService.ValidationMode.REPORT_CONSIDERING_STEALTH)
            .isValid();
   }

   private List<InternetAddress> determineToAddress() {
      try {
         String emailAddress = Optional.ofNullable(this.settings.getEmailAddress().getValue())
            .orElseThrow(() -> new FailureException("No e-mail address provided"));
         return List.of(new InternetAddress(emailAddress));
      } catch (AddressException e) {
         throw new FailureException(e);
      }
   }

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

      return switch (((EmailVerificationStepSession)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(EmailVerificationStep.EmailVerificationStepError.EMAIL_OTP_EXPIRED).build();
   }

   private AuthenticationStepResult incorrectOtp() {
      ((EmailVerificationStepSession)this.session.get()).increaseFailedAttempts();
      if (((EmailVerificationStepSession)this.session.get()).getFailedAttempts() > this.settings.getMaxFailedAttempts()) {
         LOG.info("User " + this.quotedName() + " failed to verify email address, no further attempts left");
         return this.stepResultFactory().fail(EmailVerificationStep.EmailVerificationStepError.EMAIL_OTP_WRONG).build();
      } else {
         LOG.info(
            "User "
               + this.quotedName()
               + " failed to verify email address, remaining attempts: "
               + (this.settings.getMaxFailedAttempts() - ((EmailVerificationStepSession)this.session.get()).getFailedAttempts() + 1)
         );
         return this.stepResultFactory()
            .failRetry(EmailVerificationStep.EmailVerificationStepError.EMAIL_OTP_WRONG, EmailVerificationStep.EmailVerificationStepAction.EMAIL_OTP_REQUIRED)
            .addAdditionalAttribute("emailAddress", this.maskedEmailAddress())
            .build();
      }
   }

   private AuthenticationStepResult correctOtp() {
      LOG.info("User " + this.quotedName() + " successfully verified email address");
      return this.stepResultFactory().success().build();
   }

   private String maskedEmailAddress() {
      String address = this.determineToAddress().getFirst().getAddress();
      return ((Optional)this.masking.get()).<String>map(m -> m.mask(address)).orElse(address);
   }

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

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

   private String quotedName() {
      return LogUtil.logSafeQuote(UserIdentityLogs.orElseNotAvailable(((PrincipalInfo)this.principalInfo.get()).getKnownIdentity()));
   }

   @Override
   public AuthenticationMethod getAuthenticationMethod() {
      return this.authMethod.orElseThrow(() -> new FailureException("no authentication method"));
   }
}
