package com.airlock.iam.authentication.domain.model.rememberme;

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.authentication.api.domain.model.step.IdentityProvidingFlowStep;
import com.airlock.iam.authentication.api.domain.service.username.IdentificationResult;
import com.airlock.iam.authentication.api.domain.service.username.IdentificationResultConverter;
import com.airlock.iam.authentication.application.configuration.rememberme.RememberMeUserIdentifyingStepConfig;
import com.airlock.iam.authentication.domain.service.rememberme.RememberMeTokenGenerationService;
import com.airlock.iam.authentication.domain.service.rememberme.RememberMeTokenVerificationService;
import com.airlock.iam.authentication.impl.domain.model.authentication.UserIdentificationResultData;
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.base.misc.result.Result;
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.user.UserIdentity;
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.shared.domain.model.rememberme.RememberMeSecret;
import com.airlock.iam.flow.shared.domain.model.rememberme.RememberMeSecretHash;
import com.airlock.iam.flow.shared.domain.model.rememberme.RememberMeToken;
import com.airlock.iam.flow.shared.domain.model.rememberme.RememberMeTokenLifetime;
import com.airlock.iam.flow.shared.domain.model.rememberme.RememberMeTokenRepository;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.log.api.signing.SigningLogger;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;

@LogicForConfig(RememberMeUserIdentifyingStepConfig.class)
public final class RememberMeUserIdentifyingStep implements AuthenticationFlowStep, IdentityProvidingFlowStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(RememberMeUserIdentifyingStep.class);
   private final RememberMeTokenVerificationService tokenVerificationService;
   private final RememberMeTokenGenerationService tokenGenerationService;
   private final Provider<RememberMeRequest> request;
   private final Provider<RememberMeResponse> response;
   private final RememberMeTokenRepository tokenRepository;
   private final Provider<RememberMeCookie.RememberMeCookieBuilder> preconfiguredCookieBuilder;
   private final RememberMeTokenLifetime tokenLifetime;
   private final SigningLogger signingLogger;
   private final HelpdeskLogger helpdeskLogger;
   private final TimeProvider time;
   private final AuthenticationMethod authMethod;

   @Inject
   public RememberMeUserIdentifyingStep(
      RememberMeTokenVerificationService tokenVerificationService,
      RememberMeTokenGenerationService tokenGenerationService,
      Provider<RememberMeRequest> request,
      Provider<RememberMeResponse> response,
      RememberMeTokenRepository tokenRepository,
      Provider<RememberMeCookie.RememberMeCookieBuilder> preconfiguredCookieBuilder,
      RememberMeTokenLifetime tokenLifetime,
      SigningLogger signingLogger,
      HelpdeskLogger helpdeskLogger,
      TimeProvider time,
      AuthenticationMethod authMethod
   ) {
      this.tokenVerificationService = tokenVerificationService;
      this.tokenGenerationService = tokenGenerationService;
      this.request = request;
      this.response = response;
      this.tokenRepository = tokenRepository;
      this.preconfiguredCookieBuilder = preconfiguredCookieBuilder;
      this.tokenLifetime = tokenLifetime;
      this.signingLogger = signingLogger;
      this.helpdeskLogger = helpdeskLogger;
      this.time = time;
      this.authMethod = authMethod;
   }

   @Override
   public AuthenticationStepResult initialize() {
      Optional<RememberMeJspSecret> jspSecret = ((RememberMeRequest)this.request.get()).findRememberMeJspSecret();
      return jspSecret.isPresent() ? this.verifyAndMigrateJspRememberMeToken(jspSecret.get()) : this.findAndVerifyFlowRememberMeSecret();
   }

   private AuthenticationStepResult findAndVerifyFlowRememberMeSecret() {
      Optional<RememberMeSecret> optionalSecret = ((RememberMeRequest)this.request.get()).findRememberMeSecret();
      if (optionalSecret.isEmpty()) {
         LOG.debug(String.format("No Remember-Me cookie found in request. Skipping '%s'.", this.getDescription()));
         return this.skip();
      } else {
         return this.verifyRememberMeSecret(optionalSecret.get());
      }
   }

   private AuthenticationStepResult verifyRememberMeSecret(RememberMeSecret secret) {
      Result<RememberMeToken, RememberMeTokenVerificationService.RememberMeVerificationFailure> result = this.tokenVerificationService.verifyToken(secret);
      if (result.isFailure()) {
         this.handleFailedTokenVerification(result.getFailure(), secret.hash());
         this.signingLogger
            .log(
               "Skipping '%s' due to invalid Remember-Me token with hash %s.",
               new Object[]{this.getDescription(), LogUtil.logSafeQuote(secret.hash().toString())}
            );
         return this.skip();
      }

      RememberMeToken token = result.get();
      IdentificationResult identificationResult = this.tokenVerificationService.identifyTokenOwner(token.iamUserId());
      if (identificationResult.isFailure()) {
         IdentificationResult.IdentificationFailure identificationFailure = identificationResult.getFailure().orElseThrow();
         if (IdentificationResult.IdentificationFailure.IdentificationError.CONCURRENT_ACCESS.equals(identificationFailure.error())) {
            LOG.info("Concurrent access during Remember-Me token verification.");
            return IdentificationResultConverter.convertToAuthStepResult(identificationFailure, this.stepResultFactory()).build();
         } else {
            return this.identificationFailedAndSkipStep(token.iamUserId());
         }
      } else {
         RememberMeSecretHash oldTokenHash = token.secretHash();
         RememberMeTokenResult tokenResult = this.tokenGenerationService.renewToken(token);
         ((RememberMeResponse)this.response.get()).addRememberMeCookie(this.renewedCookie(tokenResult.token(), tokenResult.secret()));
         return this.identificationSuccess(identificationResult, oldTokenHash, tokenResult.token());
      }
   }

   private AuthenticationStepResult verifyAndMigrateJspRememberMeToken(RememberMeJspSecret secret) {
      ((RememberMeResponse)this.response.get()).deleteJspRememberMeCookie();
      Optional<RememberMeJspToken> result = this.tokenVerificationService.verifyJspToken(secret);
      if (result.isEmpty()) {
         this.signingLogger.log("Skipping '%s' due to invalid JSP Remember-Me token.", new Object[]{this.getDescription()});
         return this.skip();
      }

      RememberMeJspToken token = result.get();
      UserId userId = token.iamUserId();
      IdentificationResult identificationResult = this.tokenVerificationService.identifyTokenOwner(token.iamUserId());
      if (identificationResult.isFailure()) {
         return this.identificationFailedAndSkipStep(userId);
      }

      RememberMeTokenResult newTokenResult = this.tokenGenerationService.createToken(userId);
      this.log(userId, String.format("Remember-Me token migration for user %s successful.", LogUtil.logSafeQuote(userId)));
      ((RememberMeResponse)this.response.get()).addRememberMeCookie(this.renewedCookie(newTokenResult.token(), newTokenResult.secret()));
      return this.identificationFromJspSuccess(identificationResult, newTokenResult.token());
   }

   private RememberMeCookie renewedCookie(RememberMeToken token, RememberMeSecret newSecret) {
      Instant expiresAt = token.firstUsedAt().plus(this.tokenLifetime.lifetime());
      return ((RememberMeCookie.RememberMeCookieBuilder)this.preconfiguredCookieBuilder.get())
         .maxAge((int)Duration.between(this.time.getCurrentInstant(), expiresAt).toSeconds())
         .value(newSecret.base64Url())
         .build();
   }

   private void handleFailedTokenVerification(RememberMeTokenVerificationService.RememberMeVerificationFailure failure, RememberMeSecretHash hash) {
      switch (failure) {
         case REMEMBER_ME_TOKEN_NOT_FOUND:
            LOG.debug(String.format("Retrieved unknown Remember-Me token hash: %s", hash.toString()));
            break;
         case REMEMBER_ME_TOKEN_EXPIRED:
            LOG.debug(String.format("Retrieved expired Remember-Me token hash: %s", hash.toString()));
            this.tokenRepository.deleteBy(hash);
            break;
         default:
            throw new FailureException("Unexpected token verification failure: " + failure);
      }

      ((RememberMeResponse)this.response.get()).deleteRememberMeCookie();
   }

   private AuthenticationStepResult identificationFailedAndSkipStep(UserId userId) {
      this.log(userId, String.format("Identification of user %s failed.", LogUtil.logSafeQuote(userId)));
      return this.skip();
   }

   private AuthenticationStepResult identificationSuccess(
      IdentificationResult identificationResult, RememberMeSecretHash oldTokenHash, RememberMeToken renewedToken
   ) {
      UserIdentity userIdentity = identificationResult.getUserIdentity().orElseThrow();
      this.log(
         userIdentity.id(),
         String.format(
            "User %s successfully identified and renewed Remember-Me token with ID %s and old hash %s to new hash %s",
            LogUtil.logSafeQuote(userIdentity.id()),
            LogUtil.quote(renewedToken.getId()),
            LogUtil.quote(oldTokenHash),
            LogUtil.quote(renewedToken.secretHash())
         )
      );
      return this.stepResultFactory().success().internalResultData(new UserIdentificationResultData(userIdentity)).build();
   }

   private AuthenticationStepResult identificationFromJspSuccess(IdentificationResult identificationResult, RememberMeToken renewedToken) {
      UserIdentity userIdentity = identificationResult.getUserIdentity().orElseThrow();
      this.log(
         userIdentity.id(),
         String.format(
            "User %s successfully identified and renewed JSP Remember-Me token to new token with ID %s and hash %s",
            LogUtil.logSafeQuote(userIdentity.id()),
            LogUtil.quote(renewedToken.getId()),
            LogUtil.quote(renewedToken.secretHash())
         )
      );
      return this.stepResultFactory().success().internalResultData(new UserIdentificationResultData(userIdentity)).build();
   }

   private AuthenticationStepResult skip() {
      return this.stepResultFactory().skip().build();
   }

   private void log(UserId userId, String message) {
      this.signingLogger.log(message);
      this.helpdeskLogger.log(userId.getValue(), message);
   }

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

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

   @Override
   public String getDescription() {
      return "Remember-Me User Identifying Step";
   }
}
