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

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.IdentityProvidingService;
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.misc.log.LogUtil;
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.user.PrincipalInfo;
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.StepSession;
import com.airlock.iam.flow.domain.model.step.DynamicStepError;
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 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.DeviceTokenIdentityVerificationStepConfig;
import com.airlock.iam.publicselfservice.domain.service.PublicSelfServiceUserFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;

@LogicForConfig(DeviceTokenIdentityVerificationStepConfig.class)
public final class DeviceTokenIdentityVerificationStep implements DeviceTokenVerificationStep, IdentityProvidingFlowStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(DeviceTokenIdentityVerificationStep.class);
   private final Provider<PrincipalInfo> principalInfo;
   private final Provider<PublicSelfServiceUser> user;
   private final Provider<PublicSelfServiceUserFactory> userFactory;
   private final Provider<DeviceTokenAuthStepSession> stepSession;
   private final Provider<IdentityProvidingService> identificationService;
   private final Provider<DeviceTokenAuthenticationService> deviceTokenAuthenticationService;
   private final Provider<AuthenticationMethod> authMethod;

   @Inject
   public DeviceTokenIdentityVerificationStep(
      Provider<PublicSelfServiceUser> user,
      Provider<PublicSelfServiceUserFactory> userFactory,
      Provider<DeviceTokenAuthStepSession> stepSession,
      Provider<PrincipalInfo> principalInfo,
      Provider<IdentityProvidingService> identificationService,
      Provider<DeviceTokenAuthenticationService> deviceTokenAuthenticationService,
      Provider<AuthenticationMethod> authMethod
   ) {
      this.user = user;
      this.userFactory = userFactory;
      this.stepSession = stepSession;
      this.principalInfo = principalInfo;
      this.identificationService = identificationService;
      this.deviceTokenAuthenticationService = deviceTokenAuthenticationService;
      this.authMethod = authMethod;
   }

   @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 (((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 ((PrincipalInfo)this.principalInfo.get()).getKnownIdentity().isPresent()
         ? this.checkResponseForIdentifiedUser(responseJwt, challenge)
         : this.identifyUserAndCheckResponse(responseJwt, challenge);
   }

   private AuthenticationStepResult checkResponseForIdentifiedUser(String responseJwt, DeviceTokenChallenge challenge) {
      if (!((PublicSelfServiceUser)this.user.get()).isPublicSelfServiceAllowed()) {
         return this.stepErrorForRestrictedUser((PublicSelfServiceUser)this.user.get());
      } else {
         String username = ((PublicSelfServiceUser)this.user.get()).getIdentity().id().getValue();
         if (!this.isResponseCorrect(responseJwt, username, challenge)) {
            LOG.info(String.format("Device token verification for user %s failed.", LogUtil.logSafeQuote(username)));
            return this.stepResultFactory().fail(DeviceTokenVerificationStep.DeviceTokenVerificationStepErrors.DEVICE_TOKEN_VERIFICATION_FAILED).build();
         } else {
            return this.stepResultFactory().success().build();
         }
      }
   }

   private AuthenticationStepResult identifyUserAndCheckResponse(String responseJwt, DeviceTokenChallenge challenge) {
      Optional<UserIdentity> identifyResult = this.identifyUserWithDeviceId(challenge.getDeviceTokenId());
      if (identifyResult.isEmpty()) {
         return this.stepResultFactory().fail(DeviceTokenVerificationStep.DeviceTokenVerificationStepErrors.DEVICE_TOKEN_VERIFICATION_FAILED).build();
      } else {
         PublicSelfServiceUser createdPublicSelfServiceUser = ((PublicSelfServiceUserFactory)this.userFactory.get())
            .createPublicSelfServiceUser(identifyResult.get());
         if (!createdPublicSelfServiceUser.isPublicSelfServiceAllowed()) {
            return this.stepErrorForRestrictedUser(createdPublicSelfServiceUser);
         } else {
            String username = identifyResult.get().id().getValue();
            if (!this.isResponseCorrect(responseJwt, username, challenge)) {
               LOG.info(String.format("Device token verification for user %s failed.", LogUtil.logSafeQuote(username)));
               return this.stepResultFactory().fail(DeviceTokenVerificationStep.DeviceTokenVerificationStepErrors.DEVICE_TOKEN_VERIFICATION_FAILED).build();
            } else {
               return this.stepResultFactory().asAuthenticationStepResult(this.userIdentifiedSuccessResult(identifyResult.get())).build();
            }
         }
      }
   }

   private Optional<UserIdentity> identifyUserWithDeviceId(String deviceTokenId) {
      Optional<String> username = ((DeviceTokenAuthenticationService)this.deviceTokenAuthenticationService.get()).findUserWithDeviceToken(deviceTokenId);
      if (username.isEmpty()) {
         LOG.info("No valid and enabled token found for device token " + deviceTokenId + ".");
         return Optional.empty();
      } else {
         IdentificationResult identifyResult = ((IdentityProvidingService)this.identificationService.get())
            .tryIdentify(ProvidedUsername.providedUsernameOf(username.get()));
         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(),
                     deviceTokenId,
                     identifyResult.getFailure().orElseThrow().error().name()
                  )
               )
            );
         return Optional.of(userIdentity);
      }
   }

   private PublicSelfServiceRestriction.ViolationCode getViolationCode(PublicSelfServiceUser publicSelfServiceUser) {
      return publicSelfServiceUser.getViolationIfPresent().orElseThrow(() -> new FailureException("Public self-service not allowed but no violation found."));
   }

   private AuthenticationStepResult stepErrorForRestrictedUser(PublicSelfServiceUser publicSelfServiceUser) {
      LOG.info("Rejecting user identification because of a public self-service restriction violation: " + this.getViolationCode(publicSelfServiceUser));
      Optional<PublicSelfServiceRestriction.ViolationCode> violationToReport = publicSelfServiceUser.getViolationToReport();
      return violationToReport.isPresent()
         ? this.stepResultFactory().fail(DynamicStepError.dynamicStepError(violationToReport.get().name())).isFailedFactor(false).build()
         : this.stepResultFactory()
            .fail(DeviceTokenVerificationStep.DeviceTokenVerificationStepErrors.DEVICE_TOKEN_VERIFICATION_FAILED)
            .isFailedFactor(false)
            .build();
   }

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

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

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

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class DeviceTokenIdentityVerificationStepBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<PublicSelfServiceUser> user;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<PublicSelfServiceUserFactory> userFactory;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<DeviceTokenAuthStepSession> stepSession;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<PrincipalInfo> principalInfo;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<IdentityProvidingService> identificationService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<DeviceTokenAuthenticationService> deviceTokenAuthenticationService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AuthenticationMethod> authMethod;

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

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenIdentityVerificationStep.DeviceTokenIdentityVerificationStepBuilder user(Provider<PublicSelfServiceUser> user) {
         this.user = user;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenIdentityVerificationStep.DeviceTokenIdentityVerificationStepBuilder userFactory(Provider<PublicSelfServiceUserFactory> userFactory) {
         this.userFactory = userFactory;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenIdentityVerificationStep.DeviceTokenIdentityVerificationStepBuilder principalInfo(Provider<PrincipalInfo> principalInfo) {
         this.principalInfo = principalInfo;
         return this;
      }

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenIdentityVerificationStep build() {
         return new DeviceTokenIdentityVerificationStep(
            this.user,
            this.userFactory,
            this.stepSession,
            this.principalInfo,
            this.identificationService,
            this.deviceTokenAuthenticationService,
            this.authMethod
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "DeviceTokenIdentityVerificationStep.DeviceTokenIdentityVerificationStepBuilder(user="
            + this.user
            + ", userFactory="
            + this.userFactory
            + ", stepSession="
            + this.stepSession
            + ", principalInfo="
            + this.principalInfo
            + ", identificationService="
            + this.identificationService
            + ", deviceTokenAuthenticationService="
            + this.deviceTokenAuthenticationService
            + ", authMethod="
            + this.authMethod
            + ")";
      }
   }
}
