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

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.IdentityProvidingService;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.user.ProvidedUsername;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.flow.FlowId;
import com.airlock.iam.flow.api.domain.model.redflag.RedFlag;
import com.airlock.iam.flow.api.domain.model.session.FlowSession;
import com.airlock.iam.flow.api.domain.model.step.StepError;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.domain.model.step.DynamicStepError;
import com.airlock.iam.flow.shared.domain.model.continuation.FlowContinuationInformation;
import com.airlock.iam.flow.shared.domain.model.continuation.FlowContinuationToken;
import com.airlock.iam.flow.shared.domain.service.continuation.FlowContinuationTokenService;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.airlock.iam.publicselfservice.api.domain.model.PublicSelfServiceRestriction;
import com.airlock.iam.publicselfservice.api.domain.model.PublicSelfServiceUser;
import com.airlock.iam.publicselfservice.application.configuration.steps.FlowContinuationStepConfig;
import com.airlock.iam.publicselfservice.domain.service.PublicSelfServiceUserFactory;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.time.Instant;
import java.util.Optional;

@LogicForConfig(FlowContinuationStepConfig.class)
public final class FlowContinuationStep implements IdentityProvidingFlowStep {
   public static final RedFlag FLOW_CONTINUATION_TOKEN_NOT_CONSUMED_RED_FLAG = RedFlag.builder().name("FLOW_CONTINUATION_TOKEN_NOT_CONSUMED").build();
   private static final ELogger LOG = ELoggerFactory.getLogger(FlowContinuationStep.class);
   private final Provider<Optional<FlowContinuationToken>> tokenFromRequest;
   private final Provider<FlowContinuationTokenService> tokenService;
   private final Provider<IdentityProvidingService> identificationService;
   private final Provider<PublicSelfServiceUserFactory> userFactory;
   private final Provider<FlowId> currentFlowId;
   private final Provider<FlowContinuationStepSettings> stepSettings;
   private final Provider<FlowSession> flowSession;
   private final HelpdeskLogger helpdeskLogger;
   private final SigningLogger signingLogger;

   @Inject
   public FlowContinuationStep(
      Provider<Optional<FlowContinuationToken>> tokenFromRequest,
      Provider<FlowContinuationTokenService> tokenService,
      Provider<IdentityProvidingService> identificationService,
      Provider<PublicSelfServiceUserFactory> userFactory,
      Provider<FlowId> currentFlowId,
      Provider<FlowContinuationStepSettings> stepSettings,
      Provider<FlowSession> flowSession,
      HelpdeskLogger helpdeskLogger,
      SigningLogger signingLogger
   ) {
      this.tokenService = tokenService;
      this.identificationService = identificationService;
      this.userFactory = userFactory;
      this.tokenFromRequest = tokenFromRequest;
      this.currentFlowId = currentFlowId;
      this.stepSettings = stepSettings;
      this.flowSession = flowSession;
      this.helpdeskLogger = helpdeskLogger;
      this.signingLogger = signingLogger;
   }

   @Override
   public StepResult initialize() {
      Optional<FlowContinuationToken> token = (Optional<FlowContinuationToken>)this.tokenFromRequest.get();
      if (token.isEmpty()) {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "Expected flow continuation token hash in request header but found none.");
         return this.stepResultFactory().fail(FlowContinuationStep.FlowContinuationStepErrors.FLOW_CONTINUATION_NOT_ALLOWED).build();
      }

      Optional<FlowContinuationInformation> continuationInfo = ((FlowContinuationTokenService)this.tokenService.get()).resolveContinuationToken(token.get());
      if (continuationInfo.isEmpty()) {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "No information found for provided token to continue flow.");
         return this.stepResultFactory().fail(FlowContinuationStep.FlowContinuationStepErrors.FLOW_CONTINUATION_NOT_ALLOWED).build();
      }

      FlowContinuationInformation info = continuationInfo.get();
      if (!info.targetFlowId().equals(this.currentFlowId.get())) {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "The current flow ID does not match the one from the continuation token.");
         return this.stepResultFactory().fail(FlowContinuationStep.FlowContinuationStepErrors.FLOW_CONTINUATION_NOT_ALLOWED).build();
      }

      if (Instant.now().isAfter(info.validUntil())) {
         LOG.warn(
            EDefaultLogType.GENERIC_LOG_TYPE,
            String.format("Not allowed to continue flow with ID %s because the token has expired.", info.targetFlowId().getFlowId())
         );
         return this.stepResultFactory().fail(FlowContinuationStep.FlowContinuationStepErrors.FLOW_CONTINUATION_TOKEN_EXPIRED).build();
      }

      IdentificationResult identifyResult = ((IdentityProvidingService)this.identificationService.get())
         .tryIdentify(ProvidedUsername.providedUsernameOf(info.userId().getValue()));
      if (identifyResult.hasErrorIn(
         IdentificationResult.IdentificationFailure.IdentificationError.USER_NOT_FOUND,
         IdentificationResult.IdentificationFailure.IdentificationError.USERNAME_INVALID,
         IdentificationResult.IdentificationFailure.IdentificationError.CONCURRENT_ACCESS,
         IdentificationResult.IdentificationFailure.IdentificationError.DIFFERENT_USERNAME
      )) {
         LOG.warn(
            EDefaultLogType.GENERIC_LOG_TYPE,
            String.format(
               "Not allowed to continue flow with ID %s because the user %s could not be identified.", info.targetFlowId().getFlowId(), info.userId()
            )
         );
         return this.stepResultFactory().fail(FlowContinuationStep.FlowContinuationStepErrors.USER_NOT_FOUND).build();
      }

      if (((FlowContinuationStepSettings)this.stepSettings.get()).consumesToken()) {
         ((FlowContinuationTokenService)this.tokenService.get()).deleteContinuationToken(info.tokenHash());
         LOG.debug("Successfully consumed flow continuation token.");
      } else {
         ((FlowSession)this.flowSession.get()).raiseRedFlag(FLOW_CONTINUATION_TOKEN_NOT_CONSUMED_RED_FLAG);
         ((FlowSession)this.flowSession.get()).flowAttributes().putOrMergeAttribute(info.tokenHash());
      }

      PublicSelfServiceUser user = ((PublicSelfServiceUserFactory)this.userFactory.get())
         .createPublicSelfServiceUser(identifyResult.getUserIdentity().orElseThrow());
      Optional<PublicSelfServiceRestriction.ViolationCode> violationToReport = user.getViolationToReport();
      if (violationToReport.isPresent()) {
         LOG.info("Rejecting flow continuation because of a public self-service restriction violation: " + violationToReport.get());
         return this.stepResultFactory().fail(stepError(violationToReport.get())).build();
      } else if (!user.isPublicSelfServiceAllowed()) {
         LOG.info("Rejecting flow continuation because of a public self-service restriction violation: " + user.getViolationIfPresent().orElseThrow());
         return this.stepResultFactory().fail(FlowContinuationStep.FlowContinuationStepErrors.FLOW_CONTINUATION_NOT_ALLOWED).build();
      } else {
         this.helpdeskLogger.log(info.userId(), "Successfully identified user to continue flow.");
         this.signingLogger
            .log("Identified user %s - continuing flow with ID %s", new Object[]{LogUtil.quote(info.userId()), LogUtil.quote(info.targetFlowId().getFlowId())});
         UserIdentity identity = identifyResult.getUserIdentity().orElseThrow();
         return this.userIdentifiedSuccessResult(identity);
      }
   }

   @Override
   public String getDescription() {
      return "Flow Continuation Step";
   }

   private static StepError stepError(PublicSelfServiceRestriction.ViolationCode violationCode) {
      return DynamicStepError.dynamicStepError(violationCode.name());
   }

   enum FlowContinuationStepErrors implements StepError {
      FLOW_CONTINUATION_NOT_ALLOWED,
      FLOW_CONTINUATION_TOKEN_EXPIRED,
      USER_NOT_FOUND;
   }
}
