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

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.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.domain.model.user.PrincipalInfo;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactorDetail;
import com.airlock.iam.core.domain.model.cronto.CrontoCheckingSettings;
import com.airlock.iam.core.domain.model.cronto.CrontoOtp;
import com.airlock.iam.core.misc.cronto.CrontoHandler;
import com.airlock.iam.core.misc.impl.authen.CrontoUsedDeviceInfo;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.session.FlowAttributeRepository;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.shared.application.configuration.cronto.CommonCrontoCheckingStepConfig;
import com.airlock.iam.flow.shared.domain.model.attributes.AuthTokenIdAttribute;
import com.airlock.iam.flow.shared.domain.model.attributes.AuthTokenIdFlowSessionAttribute;
import com.airlock.iam.flow.shared.domain.model.session.PollingResultCache;
import com.airlock.iam.flow.shared.domain.service.PollingService;
import com.airlock.iam.flow.shared.domain.service.cronto.CrontoCheckingService;
import com.airlock.iam.flow.shared.domain.service.cronto.CrontoCheckingServiceImpl;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;

@LogicForConfig(CommonCrontoCheckingStepConfig.class)
public final class CrontoCheckingStepImpl implements CrontoCheckingStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(CrontoCheckingStepImpl.class);
   private final CrontoCheckingSettings settings;
   private final Provider<CrontoCheckingService> service;
   private final Provider<PrincipalInfo> user;
   private final Provider<AuthenticationMethod> authMethod;
   private final Provider<FlowAttributeRepository> flowAttributes;
   private final Provider<CrontoCheckingStepSession> stepSession;
   private final Provider<CrontoHandler> crontoHandler;
   private final PollingService<CrontoCheckingStepSession, AuthenticationStepResult> pollingService;

   @Inject
   public CrontoCheckingStepImpl(
      CrontoCheckingSettings settings,
      Provider<CrontoCheckingService> service,
      Provider<PrincipalInfo> user,
      Provider<AuthenticationMethod> authMethod,
      Provider<FlowAttributeRepository> flowAttributes,
      Provider<CrontoCheckingStepSession> stepSession,
      Provider<CrontoHandler> crontoHandler
   ) {
      this.settings = settings;
      this.service = service;
      this.user = user;
      this.authMethod = authMethod;
      this.flowAttributes = flowAttributes;
      this.stepSession = stepSession;
      this.crontoHandler = crontoHandler;
      this.pollingService = new PollingService<>(CrontoCheckingStepSession::getPollingCache, this::pollInternal, this::createQueryResult);
   }

   @Override
   public Optional<StepSession> createSession() {
      return Optional.of(new CrontoCheckingStepSession(new PollingResultCache(), CrontoCheckingServiceImpl.createNewSession(this.settings)));
   }

   @Override
   public AuthenticationStepResult initialize() {
      return !this.principalHasActiveCrontoDevice()
         ? this.stepResultFactory().fail(AuthenticationFlowStep.GeneralAuthenticationStepError.NO_VALID_TOKEN).isFailedFactor(false).build()
         : this.handleInitResult(((CrontoCheckingService)this.service.get()).initiate(this.determineAuthToken()));
   }

   public AuthenticationStepResult checkOtp(CrontoOtp otp) {
      return !((CrontoCheckingStepSession)this.stepSession.get()).isExpectingOtpVerification()
         ? this.stepResultFactory().failDueToUnexpectedCall("no Cronto OTP check possible").build()
         : this.handleVerificationResult(((CrontoCheckingService)this.service.get()).checkOtp(otp));
   }

   public AuthenticationStepResult pollOnlineValidation(boolean queryOnly) {
      return this.pollingService.poll((CrontoCheckingStepSession)this.stepSession.get(), queryOnly);
   }

   private AuthenticationStepResult pollInternal(CrontoCheckingStepSession session) {
      return !session.isOnlineVerificationPossible()
         ? this.stepResultFactory().failDueToUnexpectedCall("no Cronto online validation possible").build()
         : this.handleVerificationResult(((CrontoCheckingService)this.service.get()).pollOnlineValidation());
   }

   private AuthenticationStepResult createQueryResult(CrontoCheckingStepSession session) {
      return CrontoAuthenticationResultMapper.nextPendingResult(session.getChallenge().orElseThrow(), this.stepResultFactory()).build();
   }

   public AuthenticationStepResult selectPushDevice(String id) {
      return ((CrontoCheckingStepSession)this.stepSession.get()).getPushDeviceChoices().isEmpty()
         ? this.stepResultFactory().failDueToUnexpectedCall("no Cronto push device selection possible").build()
         : this.handleAndMapResult(((CrontoCheckingService)this.service.get()).selectPushDevice(id));
   }

   public AuthenticationStepResult selectOfflineAuthentication() {
      return ((CrontoCheckingStepSession)this.stepSession.get()).getPushDeviceChoices().isEmpty()
         ? this.stepResultFactory().failDueToUnexpectedCall("selecting an offline device is not possible").build()
         : this.handleAndMapResult(((CrontoCheckingService)this.service.get()).selectOffline());
   }

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

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

   private Optional<CrontoUsedDeviceInfo> determineAuthToken() {
      return ((FlowAttributeRepository)this.flowAttributes.get())
         .tryGetAttribute(AuthTokenIdAttribute.class)
         .filter(attr -> this.settings.getChallengeType() != CrontoHandler.CrontoChallengeType.LOGIN)
         .map(AuthTokenIdAttribute::getAuthTokenId)
         .flatMap(CrontoUsedDeviceInfo::parse);
   }

   private AuthenticationStepResult handleVerificationResult(CrontoCheckingService.CrontoCheckResult result) {
      this.writeAuthTokenIdIntoFlowAttributeIfPresent(result);
      ((CrontoCheckingStepSession)this.stepSession.get()).updateChallengeMethod(result.getUsedMethod());
      CrontoAuthChallenge challenge = ((CrontoCheckingStepSession)this.stepSession.get()).getChallenge().orElseThrow();
      if (result.isFinalResult()) {
         ((CrontoCheckingStepSession)this.stepSession.get()).clearChallenge();
      }

      return CrontoAuthenticationResultMapper.map(result, challenge, this.stepResultFactory());
   }

   private AuthenticationStepResult handleInitResult(CrontoCheckingService.CrontoInitCheckResult result) {
      result.getChallenge()
         .ifPresent(challenge -> ((CrontoCheckingStepSession)this.stepSession.get()).setChallenge(CrontoAuthenticationResultMapper.mapChallenge(challenge)));
      if (!result.getDeviceChoices().isEmpty()) {
         ((CrontoCheckingStepSession)this.stepSession.get()).setPushDeviceChoices(result.getDeviceChoices());
      }

      return CrontoAuthenticationResultMapper.map(result, this.stepResultFactory());
   }

   private AuthenticationStepResult handleAndMapResult(CrontoCheckingService.PushDeviceSelectionResult result) {
      result.getChallenge()
         .ifPresent(challenge -> ((CrontoCheckingStepSession)this.stepSession.get()).setChallenge(CrontoAuthenticationResultMapper.mapChallenge(challenge)));
      if (!result.getDeviceChoices().isEmpty()) {
         ((CrontoCheckingStepSession)this.stepSession.get()).setPushDeviceChoices(result.getDeviceChoices());
      }

      return CrontoAuthenticationResultMapper.map(result, this.stepResultFactory());
   }

   private void writeAuthTokenIdIntoFlowAttributeIfPresent(CrontoCheckingService.CrontoCheckResult result) {
      if (result.isSuccess()) {
         String authTokenId = CrontoUsedDeviceInfo.of(result.getUsedDevice().orElseThrow(), result.getUsedMethod()).toString();
         ((FlowAttributeRepository)this.flowAttributes.get()).putOrMergeAttribute(AuthTokenIdFlowSessionAttribute.authTokenIdFlowSessionAttribute(authTokenId));
      }
   }

   private boolean principalHasActiveCrontoDevice() {
      return ((CrontoHandler)this.crontoHandler.get()).hasActiveToken(this.userId());
   }

   @Override
   public void finalizeStep() {
      Optional<CrontoAuthChallenge> challenge = ((CrontoCheckingStepSession)this.stepSession.get()).getChallenge();
      if (challenge.isPresent() && challenge.get().isOnlineValidation()) {
         this.handleVerificationResult(((CrontoCheckingService)this.service.get()).cancelValidation());
         LOG.info("Cancelled Cronto online transaction for user " + LogUtil.quote(this.userId()));
      }
   }

   public AuthenticationStepResult retrieveChallenge() {
      if (((CrontoCheckingStepSession)this.stepSession.get()).getPollingCache().hasResult()) {
         return this.stepResultFactory().failDueToUnexpectedCall("Call is not allowed after the final polling result is ready.").build();
      } else {
         return ((CrontoCheckingStepSession)this.stepSession.get()).getChallenge().isEmpty()
            ? this.stepResultFactory().failDueToUnexpectedCall("no current Cronto challenge").build()
            : this.stepResultFactory()
               .stay(CrontoCheckingStep.CrontoAuthStepActions.CRONTO_OTP_REQUIRED)
               .resultData(((CrontoCheckingStepSession)this.stepSession.get()).getChallenge().get())
               .build();
      }
   }

   public AuthenticationStepResult getPushDeviceChoices() {
      return ((CrontoCheckingStepSession)this.stepSession.get()).getPushDeviceChoices().isEmpty()
         ? this.stepResultFactory().failDueToUnexpectedCall("no Cronto push device choices available").build()
         : this.stepResultFactory()
            .stay(CrontoCheckingStep.CrontoAuthStepActions.CRONTO_DEVICE_CHOICE_REQUIRED)
            .resultData(new CrontoPushDeviceChoices(((CrontoCheckingStepSession)this.stepSession.get()).getPushDeviceChoices()))
            .build();
   }

   private UserId userId() {
      return ((PrincipalInfo)this.user.get()).getKnownIdentity().orElseThrow().id();
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static CrontoCheckingStepImpl.CrontoCheckingStepImplBuilder builder() {
      return new CrontoCheckingStepImpl.CrontoCheckingStepImplBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class CrontoCheckingStepImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private CrontoCheckingSettings settings;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<CrontoCheckingService> service;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<PrincipalInfo> user;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AuthenticationMethod> authMethod;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FlowAttributeRepository> flowAttributes;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<CrontoCheckingStepSession> stepSession;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<CrontoHandler> crontoHandler;

      @SuppressFBWarnings(justification = "generated code")
      CrontoCheckingStepImplBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoCheckingStepImpl.CrontoCheckingStepImplBuilder settings(CrontoCheckingSettings settings) {
         this.settings = settings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoCheckingStepImpl.CrontoCheckingStepImplBuilder service(Provider<CrontoCheckingService> service) {
         this.service = service;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoCheckingStepImpl.CrontoCheckingStepImplBuilder user(Provider<PrincipalInfo> user) {
         this.user = user;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoCheckingStepImpl.CrontoCheckingStepImplBuilder authMethod(Provider<AuthenticationMethod> authMethod) {
         this.authMethod = authMethod;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoCheckingStepImpl.CrontoCheckingStepImplBuilder flowAttributes(Provider<FlowAttributeRepository> flowAttributes) {
         this.flowAttributes = flowAttributes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoCheckingStepImpl.CrontoCheckingStepImplBuilder stepSession(Provider<CrontoCheckingStepSession> stepSession) {
         this.stepSession = stepSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoCheckingStepImpl.CrontoCheckingStepImplBuilder crontoHandler(Provider<CrontoHandler> crontoHandler) {
         this.crontoHandler = crontoHandler;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoCheckingStepImpl build() {
         return new CrontoCheckingStepImpl(this.settings, this.service, this.user, this.authMethod, this.flowAttributes, this.stepSession, this.crontoHandler);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "CrontoCheckingStepImpl.CrontoCheckingStepImplBuilder(settings="
            + this.settings
            + ", service="
            + this.service
            + ", user="
            + this.user
            + ", authMethod="
            + this.authMethod
            + ", flowAttributes="
            + this.flowAttributes
            + ", stepSession="
            + this.stepSession
            + ", crontoHandler="
            + this.crontoHandler
            + ")";
      }
   }
}
