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

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.user.UserId;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.cronto.CrontoDeviceId;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.domain.model.cronto.event.CrontoEventFactory;
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.CrontoOtp;
import com.airlock.iam.core.domain.model.cronto.challenge.CrontoActivation1Challenge;
import com.airlock.iam.core.misc.cronto.CrontoHandler;
import com.airlock.iam.core.misc.impl.authen.CrontoUsedDeviceInfo;
import com.airlock.iam.core.misc.impl.cronto.CrontoActivationSession;
import com.airlock.iam.core.misc.impl.cronto.CrontoChallengeResult;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoDeviceToken;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.session.FlowSession;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.api.domain.model.tag.CurrentFlowTags;
import com.airlock.iam.flow.api.domain.model.tag.TagName;
import com.airlock.iam.flow.shared.application.configuration.cronto.CrontoActivationStepConfig;
import com.airlock.iam.flow.shared.domain.model.attributes.AuthTokenIdFlowSessionAttribute;
import com.airlock.iam.flow.shared.domain.service.cronto.CrontoSelfManagementService;
import com.airlock.iam.log.api.signing.SigningLogger;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;

@LogicForConfig(CrontoActivationStepConfig.class)
public class CrontoActivationStepImpl implements CrontoActivationStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(CrontoActivationStepImpl.class);
   private static final String ACTIVATION_CHALLENGE_ATTRIBUTE = "activationChallenge";
   private final Provider<PrincipalInfo> user;
   private final Provider<FlowSession> flowSession;
   private final Provider<CrontoActivationStepSession> stepSession;
   private final Provider<AuthenticationMethod> authMethod;
   private final Provider<CrontoSelfManagementService> selfManagementService;
   private final Provider<CurrentFlowTags> currentTags;
   private final Provider<Optional<TagName>> strongAuthTagName;
   private final Provider<CrontoEventFactory> eventFactory;
   private final Provider<SigningLogger> signingLogger;

   @Inject
   public CrontoActivationStepImpl(
      Provider<PrincipalInfo> user,
      Provider<FlowSession> flowSession,
      Provider<CrontoActivationStepSession> stepSession,
      Provider<AuthenticationMethod> authMethod,
      Provider<CrontoSelfManagementService> selfManagementService,
      Provider<CurrentFlowTags> currentTags,
      Provider<Optional<TagName>> strongAuthTagName,
      Provider<CrontoEventFactory> eventFactory,
      Provider<SigningLogger> signingLogger
   ) {
      this.user = user;
      this.flowSession = flowSession;
      this.stepSession = stepSession;
      this.authMethod = authMethod;
      this.selfManagementService = selfManagementService;
      this.currentTags = currentTags;
      this.strongAuthTagName = strongAuthTagName;
      this.eventFactory = eventFactory;
      this.signingLogger = signingLogger;
   }

   @Override
   public Optional<StepSession> createSession() {
      CrontoActivationStepSession session = new CrontoActivationStepSession(this.isOnScreenActivation());
      return Optional.of(session);
   }

   @Override
   public AuthenticationStepResult initialize() {
      return ((CrontoActivationStepSession)this.stepSession.get()).isOnScreenActivation()
         ? this.startOnScreenActivation()
         : this.stepResultFactory().stay(CrontoActivationStep.CrontoActivationStepActions.CRONTO_ACTIVATION_START_REQUIRED).build();
   }

   private AuthenticationStepResult startOnScreenActivation() {
      CrontoActivationSession activationSession = ((CrontoSelfManagementService)this.selfManagementService.get()).startOnScreenActivation(this.userId());
      ((CrontoActivationStepSession)this.stepSession.get()).setActivationSession(Optional.of(activationSession));
      return this.stepResultFactory()
         .stay(CrontoActivationStep.CrontoActivationStepActions.CRONTO_ACTIVATION_START_WITHOUT_LETTER_REQUIRED)
         .addAdditionalAttribute("activationChallenge", toActivationChallenge(activationSession.getChallenge1().orElseThrow()))
         .build();
   }

   public AuthenticationStepResult checkFirstActivationOtp(CrontoOtp otp) {
      if (((CrontoActivationStepSession)this.stepSession.get()).isFirstChallengeCompleted()) {
         return this.stepResultFactory().failDueToUnexpectedCall("First challenge has already been completed").build();
      }

      StartDeviceActivationResult result;
      if (((CrontoActivationStepSession)this.stepSession.get()).isOnScreenActivation()) {
         result = ((CrontoSelfManagementService)this.selfManagementService.get())
            .verifyActivationOtp(((CrontoActivationStepSession)this.stepSession.get()).getActivationSession().orElseThrow(), otp);
      } else {
         result = ((CrontoSelfManagementService)this.selfManagementService.get()).startDeviceActivation(this.userId(), otp);
      }

      if (!result.isSuccessful()) {
         return this.handleFailedActivationStart(result.getFailureReason());
      }

      this.addResultToStepSession(result);
      return this.stepResultFactory()
         .stay(CrontoActivationStep.CrontoActivationStepActions.CRONTO_ACTIVATION_COMPLETE_REQUIRED)
         .addAdditionalAttribute("activationChallenge", ((CrontoActivationStepSession)this.stepSession.get()).getActivationChallenge().orElseThrow())
         .build();
   }

   public AuthenticationStepResult checkSecondActivationOtp(CrontoOtp otp, String deviceName) {
      if (!((CrontoActivationStepSession)this.stepSession.get()).isFirstChallengeCompleted()) {
         return this.stepResultFactory().failDueToUnexpectedCall("First challenge has not yet been completed").build();
      }

      Optional<CrontoActivationSession> activationSession = ((CrontoActivationStepSession)this.stepSession.get()).getActivationSession();
      CompleteDeviceActivationResult result = ((CrontoSelfManagementService)this.selfManagementService.get())
         .completeDeviceActivation(deviceName, otp, activationSession.orElseThrow());
      if (!result.isSuccessful()) {
         return this.handleFailedActivationComplete(result.getFailureReason());
      }

      CrontoDeviceToken crontoDevice = result.getDeviceToken();
      this.publishCrontoDeviceActivated(crontoDevice.getDeviceId());
      this.logCrontoActivation(crontoDevice);
      this.addDeviceInfo(crontoDevice.getDeviceId());
      return this.stepResultFactory().success().build();
   }

   public AuthenticationStepResult retrieveFirstActivationChallenge() {
      if (((CrontoActivationStepSession)this.stepSession.get()).isFirstChallengeCompleted()) {
         return this.stepResultFactory().failDueToUnexpectedCall("First challenge has already been completed").build();
      } else {
         return !((CrontoActivationStepSession)this.stepSession.get()).isOnScreenActivation()
            ? this.stepResultFactory().failDueToUnexpectedCall("Not Cronto activation without a letter").build()
            : this.stepResultFactory()
               .stay(CrontoActivationStep.CrontoActivationStepActions.CRONTO_ACTIVATION_START_WITHOUT_LETTER_REQUIRED)
               .resultData(toActivationChallenge(((CrontoActivationStepSession)this.stepSession.get()).getFirstActivationChallenge()))
               .build();
      }
   }

   public AuthenticationStepResult retrieveSecondActivationChallenge() {
      return !((CrontoActivationStepSession)this.stepSession.get()).isFirstChallengeCompleted()
         ? this.stepResultFactory().failDueToUnexpectedCall("First challenge has not yet been completed").build()
         : this.stepResultFactory()
            .stay(CrontoActivationStep.CrontoActivationStepActions.CRONTO_ACTIVATION_COMPLETE_REQUIRED)
            .resultData(((CrontoActivationStepSession)this.stepSession.get()).getActivationChallenge().orElseThrow())
            .build();
   }

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

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

   private static CrontoActivationChallenge toActivationChallenge(CrontoActivation1Challenge crontoChallenge) {
      return new CrontoActivationChallenge(crontoChallenge.getCryptogramForScreen().base64image(), crontoChallenge.getRawChallengeString());
   }

   private AuthenticationStepResult handleFailedActivationStart(StartDeviceActivationResult.FailureReason reason) {
      if (reason == StartDeviceActivationResult.FailureReason.CRONTO_OTP_WRONG) {
         LOG.info(String.format("User \"%s\" provided invalid first OTP while activating a new Cronto device.", this.userId()));
         return ((CrontoActivationStepSession)this.stepSession.get()).isOnScreenActivation()
            ? this.stepResultFactory()
               .failRetry(toStepError(reason), CrontoActivationStep.CrontoActivationStepActions.CRONTO_ACTIVATION_START_WITHOUT_LETTER_REQUIRED)
               .addAdditionalAttribute(
                  "activationChallenge", toActivationChallenge(((CrontoActivationStepSession)this.stepSession.get()).getFirstActivationChallenge())
               )
               .build()
            : this.stepResultFactory()
               .failRetry(toStepError(reason), CrontoActivationStep.CrontoActivationStepActions.CRONTO_ACTIVATION_START_REQUIRED)
               .build();
      } else {
         return this.stepResultFactory().fail(toStepError(reason)).build();
      }
   }

   private AuthenticationStepResult handleFailedActivationComplete(CompleteDeviceActivationResult.FailureReason reason) {
      if (reason == CompleteDeviceActivationResult.FailureReason.CRONTO_DEVICE_ALREADY_EXISTS) {
         return this.stepResultFactory().fail(toStepError(reason)).build();
      }

      if (reason == CompleteDeviceActivationResult.FailureReason.INVALID_CRONTO_LABEL) {
         throw new FailureException("Invalid label returned by selfManagementService");
      }

      LOG.info(String.format("User \"%s\" provided invalid second OTP while activating a new Cronto device.", this.userId()));
      return this.stepResultFactory()
         .failRetry(toStepError(reason), CrontoActivationStep.CrontoActivationStepActions.CRONTO_ACTIVATION_COMPLETE_REQUIRED)
         .addAdditionalAttribute("activationChallenge", ((CrontoActivationStepSession)this.stepSession.get()).getActivationChallenge().orElseThrow())
         .build();
   }

   private void addResultToStepSession(StartDeviceActivationResult result) {
      CrontoActivationStepSession session = (CrontoActivationStepSession)this.stepSession.get();
      session.setActivationSession(Optional.of(result.getActivationSession()));
      session.setActivationChallenge(Optional.of(CrontoActivationChallenge.of(result.getActivationChallenge())));
   }

   private void publishCrontoDeviceActivated(CrontoDeviceId deviceId) {
      ((CrontoEventFactory)this.eventFactory.get()).publishCrontoDeviceActivatedEvent(this.userId(), deviceId);
   }

   private void logCrontoActivation(CrontoDeviceToken device) {
      ((SigningLogger)this.signingLogger.get())
         .log(
            "User \"%s\" successfully activated a new Cronto device (ID \"%s\", label \"%s\") during login",
            new Object[]{this.userId(), device.getDeviceIdForDisplay(), device.getCustomName()}
         );
   }

   private void addDeviceInfo(CrontoDeviceId deviceId) {
      CrontoUsedDeviceInfo deviceInfo = CrontoUsedDeviceInfo.of(deviceId, CrontoChallengeResult.CrontoMethod.SCAN_TAN);
      ((FlowSession)this.flowSession.get())
         .flowAttributes()
         .putOrMergeAttribute(AuthTokenIdFlowSessionAttribute.authTokenIdFlowSessionAttribute(deviceInfo.toString()));
   }

   private static CrontoActivationStep.CrontoActivationStepErrors toStepError(Enum<?> result) {
      return CrontoActivationStep.CrontoActivationStepErrors.valueOf(result.name());
   }

   private boolean isOnScreenActivation() {
      CrontoHandler.CrontoSelfRegistrationContext context = this.isUserStronglyAuthenticated()
         ? CrontoHandler.CrontoSelfRegistrationContext.MIGRATION
         : CrontoHandler.CrontoSelfRegistrationContext.INITIAL_REGISTRATION;
      return ((CrontoSelfManagementService)this.selfManagementService.get()).canActivateDeviceOnScreen(this.userId(), context);
   }

   private boolean isUserStronglyAuthenticated() {
      return ((Optional)this.strongAuthTagName.get()).isEmpty()
         ? false
         : ((CurrentFlowTags)this.currentTags.get()).checkTagPresent((TagName)((Optional)this.strongAuthTagName.get()).get());
   }

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

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