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

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.api.domain.service.username.IdentityProvidingService;
import com.airlock.iam.authentication.application.configuration.devicetoken.DeviceTokenAuthStepConfig;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.user.ProvidedUsername;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.domain.model.principal.UserPrincipal;
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.GenericStepError;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.shared.domain.model.devicetoken.CheckDeviceTokenJwtRequest;
import com.airlock.iam.flow.shared.domain.model.devicetoken.DeviceTokenAuthStepSession;
import com.airlock.iam.flow.shared.domain.model.devicetoken.DeviceTokenChallenge;
import com.airlock.iam.flow.shared.domain.model.devicetoken.DeviceTokenVerificationStep;
import com.airlock.iam.flow.shared.domain.service.devicetoken.DeviceTokenAuthenticationService;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;

@LogicForConfig(DeviceTokenAuthStepConfig.class)
public final class DeviceTokenAuthenticationStep implements DeviceTokenVerificationStep, IdentityProvidingFlowStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(DeviceTokenAuthenticationStep.class);
   private final Provider<DeviceTokenAuthStepSession> stepSession;
   private final Provider<UserPrincipal> userPrincipal;
   private final Provider<IdentityProvidingService> identificationService;
   private final Provider<DeviceTokenAuthenticationService> deviceTokenAuthenticationService;
   private final Provider<DeviceTokenAuthStepSettings> stepSettings;

   @Inject
   public DeviceTokenAuthenticationStep(
      Provider<DeviceTokenAuthStepSession> stepSession,
      Provider<UserPrincipal> userPrincipal,
      Provider<IdentityProvidingService> identificationService,
      Provider<DeviceTokenAuthenticationService> deviceTokenAuthenticationService,
      Provider<DeviceTokenAuthStepSettings> stepSettings
   ) {
      this.stepSession = stepSession;
      this.userPrincipal = userPrincipal;
      this.identificationService = identificationService;
      this.deviceTokenAuthenticationService = deviceTokenAuthenticationService;
      this.stepSettings = stepSettings;
   }

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

   @Override
   public AuthenticationStepResult initialize() {
      return this.stepResultFactory().stay(DeviceTokenVerificationStep.DeviceTokenVerificationStepActions.DEVICE_TOKEN_RESPONSE_REQUIRED).build();
   }

   public AuthenticationStepResult getChallenge(String deviceId) {
      if (((UserPrincipal)this.userPrincipal.get()).exists()
         && !((DeviceTokenAuthenticationService)this.deviceTokenAuthenticationService.get())
            .hasEnabledAndValidDeviceToken(((UserPrincipal)this.userPrincipal.get()).getName(), deviceId)) {
         return this.stepResultFactory().fail(GenericStepError.NO_VALID_TOKEN).build();
      }

      if (((DeviceTokenAuthStepSettings)this.stepSettings.get()).failOnChallengeRetrieve()
         && !((UserPrincipal)this.userPrincipal.get()).exists()
         && this.isUnknown(deviceId)) {
         return this.stepResultFactory().fail(GenericStepError.NO_VALID_TOKEN).isFailedFactor(false).build();
      }

      if (((DeviceTokenAuthStepSession)this.stepSession.get()).isExpectingVerification()) {
         return this.stepResultFactory().failDueToUnexpectedCall("Already created a challenge for device token step session.").build();
      }

      DeviceTokenChallenge challenge = ((DeviceTokenAuthenticationService)this.deviceTokenAuthenticationService.get()).createChallenge(deviceId);
      ((DeviceTokenAuthStepSession)this.stepSession.get()).setChallenge(challenge);
      return this.stepResultFactory()
         .stay(DeviceTokenVerificationStep.DeviceTokenVerificationStepActions.DEVICE_TOKEN_RESPONSE_REQUIRED)
         .resultData(DeviceTokenVerificationStep.DeviceTokenStepChallenge.deviceTokenChallenge(challenge.getChallenge(), challenge.getValidTo()))
         .build();
   }

   public AuthenticationStepResult checkResponseJwt(String responseJwt) {
      if (!((DeviceTokenAuthStepSession)this.stepSession.get()).isExpectingVerification()) {
         return this.stepResultFactory().failDueToUnexpectedCall("No device token challenge in session. Get a challenge first.").build();
      }

      DeviceTokenChallenge challenge = ((DeviceTokenAuthStepSession)this.stepSession.get()).retrieveChallenge();
      return ((UserPrincipal)this.userPrincipal.get()).exists()
         ? this.checkResponseForIdentifiedUser(responseJwt, challenge)
         : this.checkResponseAndIdentifyUser(responseJwt, challenge);
   }

   private boolean isUnknown(String deviceId) {
      return ((DeviceTokenAuthenticationService)this.deviceTokenAuthenticationService.get()).findUserWithDeviceToken(deviceId).isEmpty();
   }

   private AuthenticationStepResult checkResponseForIdentifiedUser(String responseJwt, DeviceTokenChallenge challenge) {
      return this.isResponseCorrect(responseJwt, ((UserPrincipal)this.userPrincipal.get()).getName(), challenge)
         ? this.stepResultFactory().success().build()
         : this.stepResultFactory().fail(DeviceTokenVerificationStep.DeviceTokenVerificationStepErrors.DEVICE_TOKEN_VERIFICATION_FAILED).build();
   }

   private AuthenticationStepResult checkResponseAndIdentifyUser(String responseJwt, DeviceTokenChallenge challenge) {
      Optional<String> username = ((DeviceTokenAuthenticationService)this.deviceTokenAuthenticationService.get())
         .findUserWithDeviceToken(challenge.getDeviceTokenId());
      if (username.isEmpty()) {
         LOG.info("No valid and enabled token found for device token " + challenge.getDeviceTokenId() + ".");
         return this.stepResultFactory().fail(DeviceTokenVerificationStep.DeviceTokenVerificationStepErrors.DEVICE_TOKEN_VERIFICATION_FAILED).build();
      }

      IdentificationResult identifyResult = ((IdentityProvidingService)this.identificationService.get())
         .tryIdentify(ProvidedUsername.providedUsernameOf(username.get()));
      if (identifyResult.isFailure()) {
         return IdentificationResultConverter.convertToAuthStepResult(
               identifyResult.getFailure().orElseThrow(),
               this.stepResultFactory(),
               DeviceTokenVerificationStep.DeviceTokenVerificationStepActions.DEVICE_TOKEN_RESPONSE_REQUIRED
            )
            .build();
      }

      UserIdentity userIdentity = identifyResult.getUserIdentity()
         .orElseThrow(
            () -> new FailureException(
               String.format(
                  "The user named %s which was retrieved for device token %s could not be identified. Reason: %s.",
                  username.get(),
                  challenge.getDeviceTokenId(),
                  identifyResult.getFailure().orElseThrow().error().name()
               )
            )
         );
      return this.isResponseCorrect(responseJwt, username.get(), challenge)
         ? this.stepResultFactory().asAuthenticationStepResult(this.userIdentifiedSuccessResult(userIdentity)).build()
         : this.stepResultFactory().fail(DeviceTokenVerificationStep.DeviceTokenVerificationStepErrors.DEVICE_TOKEN_VERIFICATION_FAILED).build();
   }

   private boolean isResponseCorrect(String responseJwt, String username, DeviceTokenChallenge challenge) {
      return ((DeviceTokenAuthenticationService)this.deviceTokenAuthenticationService.get())
         .isDeviceTokenJwtCorrect(CheckDeviceTokenJwtRequest.builder(username, responseJwt).build(), challenge);
   }

   @Override
   public AuthenticationMethod getAuthenticationMethod() {
      return ((DeviceTokenAuthStepSettings)this.stepSettings.get()).authenticationMethod();
   }

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

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class DeviceTokenAuthenticationStepBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<DeviceTokenAuthStepSession> stepSession;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<UserPrincipal> userPrincipal;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<IdentityProvidingService> identificationService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<DeviceTokenAuthenticationService> deviceTokenAuthenticationService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<DeviceTokenAuthStepSettings> stepSettings;

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

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenAuthenticationStep.DeviceTokenAuthenticationStepBuilder stepSession(Provider<DeviceTokenAuthStepSession> stepSession) {
         this.stepSession = stepSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenAuthenticationStep.DeviceTokenAuthenticationStepBuilder userPrincipal(Provider<UserPrincipal> userPrincipal) {
         this.userPrincipal = userPrincipal;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenAuthenticationStep.DeviceTokenAuthenticationStepBuilder identificationService(Provider<IdentityProvidingService> identificationService) {
         this.identificationService = identificationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenAuthenticationStep.DeviceTokenAuthenticationStepBuilder deviceTokenAuthenticationService(
         Provider<DeviceTokenAuthenticationService> deviceTokenAuthenticationService
      ) {
         this.deviceTokenAuthenticationService = deviceTokenAuthenticationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenAuthenticationStep.DeviceTokenAuthenticationStepBuilder stepSettings(Provider<DeviceTokenAuthStepSettings> stepSettings) {
         this.stepSettings = stepSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenAuthenticationStep build() {
         return new DeviceTokenAuthenticationStep(
            this.stepSession, this.userPrincipal, this.identificationService, this.deviceTokenAuthenticationService, this.stepSettings
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "DeviceTokenAuthenticationStep.DeviceTokenAuthenticationStepBuilder(stepSession="
            + this.stepSession
            + ", userPrincipal="
            + this.userPrincipal
            + ", identificationService="
            + this.identificationService
            + ", deviceTokenAuthenticationService="
            + this.deviceTokenAuthenticationService
            + ", stepSettings="
            + this.stepSettings
            + ")";
      }
   }
}
