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

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.fido.FidoPasswordlessAuthenticationStepConfig;
import com.airlock.iam.authentication.impl.domain.model.authentication.UserIdentificationResultData;
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.result.Result;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.user.ProvidedUsername;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactorDetail;
import com.airlock.iam.core.domain.model.authentication.method.KnownAuthenticationMethods;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.factor.domain.model.fido.FidoAuthTokenId;
import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorUnverifiedAssertionResponse;
import com.airlock.iam.factor.domain.model.fido.FidoClientFailure;
import com.airlock.iam.factor.domain.model.fido.FidoPublicKeyCredentialRequestOptions;
import com.airlock.iam.factor.domain.model.fido.FidoRegisteredCredential;
import com.airlock.iam.factor.domain.model.fido.FidoRelyingParty;
import com.airlock.iam.factor.domain.model.fido.FidoUserRepository;
import com.airlock.iam.factor.domain.model.fido.user.FidoPasswordlessUser;
import com.airlock.iam.factor.domain.model.fido.user.FidoUserHandle;
import com.airlock.iam.factor.domain.model.fido.user.FidoUserPrincipal;
import com.airlock.iam.factor.domain.service.fido.FidoAuthenticationService;
import com.airlock.iam.fido.api.domain.model.FidoRelyingPartyId;
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.domain.model.attributes.AuthTokenIdFlowSessionAttribute;
import com.airlock.iam.flow.shared.domain.model.fido.FidoCheckingStep;
import com.airlock.iam.flow.shared.domain.model.fido.FidoCheckingStepWithRetrieve;
import com.airlock.iam.flow.shared.domain.model.fido.FidoLoginLogger;
import com.airlock.iam.flow.shared.domain.model.fido.FidoPublicKeyCredentialRequestOptionData;
import com.airlock.iam.flow.shared.domain.model.fido.FidoStepErrorMapper;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;
import lombok.NonNull;

@LogicForConfig(FidoPasswordlessAuthenticationStepConfig.class)
public final class FidoPasswordlessAuthenticationStepImpl implements FidoCheckingStepWithRetrieve, IdentityProvidingFlowStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(FidoPasswordlessAuthenticationStepImpl.class);
   private static final String FIDO_CHALLENGE_ATTRIBUTE = "fidoChallenge";
   private final Provider<IdentityProvidingService> identificationService;
   private final Provider<FidoChallengeSession> stepSession;
   private final Provider<FidoAuthenticationService> service;
   private final Provider<FidoUserRepository> userRepository;
   private final Provider<FidoRelyingPartyId> relyingPartyId;
   private final Provider<FidoCheckingStep.FidoStepActions> stepActions;
   private final Provider<FidoCheckingStep.FidoStepErrors> stepErrors;
   private final Provider<FlowAttributeRepository> flowAttributes;
   private final Provider<FidoLoginLogger> logger;

   @Inject
   FidoPasswordlessAuthenticationStepImpl(
      @NonNull Provider<IdentityProvidingService> identificationService,
      @NonNull Provider<FidoChallengeSession> stepSession,
      @NonNull Provider<FidoAuthenticationService> service,
      @NonNull Provider<FidoUserRepository> userRepository,
      @NonNull Provider<FidoRelyingPartyId> relyingPartyId,
      @NonNull Provider<FidoCheckingStep.FidoStepActions> stepActions,
      @NonNull Provider<FidoCheckingStep.FidoStepErrors> stepErrors,
      @NonNull Provider<FlowAttributeRepository> flowAttributes,
      @NonNull Provider<FidoLoginLogger> logger
   ) {
      if (identificationService == null) {
         throw new NullPointerException("identificationService is marked non-null but is null");
      }

      if (stepSession == null) {
         throw new NullPointerException("stepSession is marked non-null but is null");
      }

      if (service == null) {
         throw new NullPointerException("service is marked non-null but is null");
      }

      if (userRepository == null) {
         throw new NullPointerException("userRepository is marked non-null but is null");
      }

      if (relyingPartyId == null) {
         throw new NullPointerException("relyingPartyId is marked non-null but is null");
      }

      if (stepActions == null) {
         throw new NullPointerException("stepActions is marked non-null but is null");
      }

      if (stepErrors == null) {
         throw new NullPointerException("stepErrors is marked non-null but is null");
      }

      if (flowAttributes == null) {
         throw new NullPointerException("flowAttributes is marked non-null but is null");
      }

      if (logger == null) {
         throw new NullPointerException("logger is marked non-null but is null");
      }

      this.identificationService = identificationService;
      this.stepSession = stepSession;
      this.service = service;
      this.userRepository = userRepository;
      this.relyingPartyId = relyingPartyId;
      this.stepActions = stepActions;
      this.stepErrors = stepErrors;
      this.flowAttributes = flowAttributes;
      this.logger = logger;
   }

   @Override
   public AuthenticationMethod getAuthenticationMethod() {
      return KnownAuthenticationMethods.FIDO;
   }

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

   @Override
   public AuthenticationStepResult initialize() {
      FidoPublicKeyCredentialRequestOptions challenge = this.generateChallengeForPasswordless();
      return this.stepResultFactory()
         .stay(((FidoCheckingStep.FidoStepActions)this.stepActions.get()).challengeRetrievalRequired())
         .addAdditionalAttribute("fidoChallenge", challenge.asAdditionalAttribute())
         .build();
   }

   @Override
   public AuthenticationStepResult retrieveAuthenticationChallenge() {
      return this.stepResultFactory()
         .stay(((FidoCheckingStep.FidoStepActions)this.stepActions.get()).assertionResponseRequired())
         .resultData(new FidoPublicKeyCredentialRequestOptionData(((FidoChallengeSession)this.stepSession.get()).getChallenge()))
         .build();
   }

   private FidoPublicKeyCredentialRequestOptions generateChallengeForPasswordless() {
      FidoPublicKeyCredentialRequestOptions challenge = ((FidoAuthenticationService)this.service.get()).generateChallengeForPasswordless();
      ((FidoChallengeSession)this.stepSession.get()).saveChallenge(challenge);
      return challenge;
   }

   @Override
   public AuthenticationStepResult verify(FidoAuthenticatorUnverifiedAssertionResponse authenticatorResponse) {
      if (authenticatorResponse.getUserHandle().isEmpty()) {
         ((FidoLoginLogger)this.logger.get()).logPasswordlessMissingUserHandle(authenticatorResponse);
         return this.stepResultFactory().fail(((FidoCheckingStep.FidoStepErrors)this.stepErrors.get()).failed()).build();
      } else {
         FidoPasswordlessUser fidoUser = FidoPasswordlessUser.fidoPasswordlessUser(
            (FidoRelyingPartyId)this.relyingPartyId.get(), FidoUserHandle.userHandleFromBase64Url(authenticatorResponse.getUserHandle().get())
         );
         Optional<FidoUserPrincipal> claimedFidoUserPrincipal = ((FidoUserRepository)this.userRepository.get()).findBy(fidoUser.getUserHandle());
         if (claimedFidoUserPrincipal.isEmpty()) {
            ((FidoLoginLogger)this.logger.get()).logPasswordlessUnknownUserHandle(authenticatorResponse);
            return this.stepResultFactory().fail(((FidoCheckingStep.FidoStepErrors)this.stepErrors.get()).failed()).build();
         } else {
            Result<UserIdentificationResultData, AuthenticationStepResult> identificationResult = this.identifyUser(
               claimedFidoUserPrincipal.get().getIamUserId()
            );
            if (identificationResult.isFailure()) {
               return identificationResult.getFailure();
            } else {
               Result<FidoRegisteredCredential, FidoRelyingParty.FidoAuthenticationVerificationErrorCode> verificationResult = ((FidoAuthenticationService)this.service
                     .get())
                  .verifyFidoResponse(((FidoChallengeSession)this.stepSession.get()).getChallenge(), fidoUser, authenticatorResponse);
               if (verificationResult.isFailure()) {
                  ((FidoLoginLogger)this.logger.get()).logPasswordlessAuthenticationFailure(fidoUser, authenticatorResponse);
                  return this.stepResultFactory()
                     .fail(FidoStepErrorMapper.mapError(verificationResult.getFailure(), (FidoCheckingStep.FidoStepErrors)this.stepErrors.get()))
                     .build();
               } else {
                  ((FidoLoginLogger)this.logger.get()).logPasswordlessAuthenticationSuccess(verificationResult.get());
                  FidoRegisteredCredential fidoRegisteredCredential = verificationResult.get();
                  ((FlowAttributeRepository)this.flowAttributes.get())
                     .putOrMergeAttribute(
                        AuthTokenIdFlowSessionAttribute.authTokenIdFlowSessionAttribute(
                           FidoAuthTokenId.fidoAuthTokenId(fidoRegisteredCredential.getId()).formatAsString()
                        )
                     );
                  return this.stepResultFactory().success().internalResultData(identificationResult.get()).build();
               }
            }
         }
      }
   }

   @Override
   public AuthenticationStepResult reportClientFailure(FidoClientFailure failure) {
      LOG.info("FIDO client reported failure: " + failure.message());
      return this.stepResultFactory()
         .fail(FidoStepErrorMapper.mapError(failure.reason(), (FidoCheckingStep.FidoStepErrors)this.stepErrors.get()))
         .isFailedFactor(false)
         .build();
   }

   private Result<UserIdentificationResultData, AuthenticationStepResult> identifyUser(UserId claimedIamUserId) {
      ProvidedUsername providedUsername = ProvidedUsername.providedUsernameOf(claimedIamUserId.getValue());
      IdentificationResult identificationResult = ((IdentityProvidingService)this.identificationService.get()).tryIdentify(providedUsername);
      if (identificationResult.isFailure()) {
         identificationResult.getUserIdentity()
            .ifPresent(
               id -> ((FidoLoginLogger)this.logger.get())
                  .logPasswordlessIdentificationFailure(id.id(), identificationResult.getFailure().orElseThrow().error())
            );
         return Result.failure(
            IdentificationResultConverter.convertToAuthStepResult(
                  identificationResult.getFailure().orElseThrow(),
                  this.stepResultFactory(),
                  ((FidoCheckingStep.FidoStepActions)this.stepActions.get()).assertionResponseRequired()
               )
               .build()
         );
      } else {
         return Result.result(new UserIdentificationResultData(identificationResult.getUserIdentity().orElseThrow()));
      }
   }

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

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class FidoPasswordlessAuthenticationStepImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<IdentityProvidingService> identificationService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FidoChallengeSession> stepSession;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FidoAuthenticationService> service;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FidoUserRepository> userRepository;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FidoRelyingPartyId> relyingPartyId;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FidoCheckingStep.FidoStepActions> stepActions;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FidoCheckingStep.FidoStepErrors> stepErrors;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FlowAttributeRepository> flowAttributes;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FidoLoginLogger> logger;

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

      @SuppressFBWarnings(justification = "generated code")
      public FidoPasswordlessAuthenticationStepImpl.FidoPasswordlessAuthenticationStepImplBuilder identificationService(
         @NonNull Provider<IdentityProvidingService> identificationService
      ) {
         if (identificationService == null) {
            throw new NullPointerException("identificationService is marked non-null but is null");
         }

         this.identificationService = identificationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPasswordlessAuthenticationStepImpl.FidoPasswordlessAuthenticationStepImplBuilder stepSession(
         @NonNull Provider<FidoChallengeSession> stepSession
      ) {
         if (stepSession == null) {
            throw new NullPointerException("stepSession is marked non-null but is null");
         }

         this.stepSession = stepSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPasswordlessAuthenticationStepImpl.FidoPasswordlessAuthenticationStepImplBuilder service(@NonNull Provider<FidoAuthenticationService> service) {
         if (service == null) {
            throw new NullPointerException("service is marked non-null but is null");
         }

         this.service = service;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPasswordlessAuthenticationStepImpl.FidoPasswordlessAuthenticationStepImplBuilder userRepository(
         @NonNull Provider<FidoUserRepository> userRepository
      ) {
         if (userRepository == null) {
            throw new NullPointerException("userRepository is marked non-null but is null");
         }

         this.userRepository = userRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPasswordlessAuthenticationStepImpl.FidoPasswordlessAuthenticationStepImplBuilder relyingPartyId(
         @NonNull Provider<FidoRelyingPartyId> relyingPartyId
      ) {
         if (relyingPartyId == null) {
            throw new NullPointerException("relyingPartyId is marked non-null but is null");
         }

         this.relyingPartyId = relyingPartyId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPasswordlessAuthenticationStepImpl.FidoPasswordlessAuthenticationStepImplBuilder stepActions(
         @NonNull Provider<FidoCheckingStep.FidoStepActions> stepActions
      ) {
         if (stepActions == null) {
            throw new NullPointerException("stepActions is marked non-null but is null");
         }

         this.stepActions = stepActions;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPasswordlessAuthenticationStepImpl.FidoPasswordlessAuthenticationStepImplBuilder stepErrors(
         @NonNull Provider<FidoCheckingStep.FidoStepErrors> stepErrors
      ) {
         if (stepErrors == null) {
            throw new NullPointerException("stepErrors is marked non-null but is null");
         }

         this.stepErrors = stepErrors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPasswordlessAuthenticationStepImpl.FidoPasswordlessAuthenticationStepImplBuilder flowAttributes(
         @NonNull Provider<FlowAttributeRepository> flowAttributes
      ) {
         if (flowAttributes == null) {
            throw new NullPointerException("flowAttributes is marked non-null but is null");
         }

         this.flowAttributes = flowAttributes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPasswordlessAuthenticationStepImpl.FidoPasswordlessAuthenticationStepImplBuilder logger(@NonNull Provider<FidoLoginLogger> logger) {
         if (logger == null) {
            throw new NullPointerException("logger is marked non-null but is null");
         }

         this.logger = logger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPasswordlessAuthenticationStepImpl build() {
         return new FidoPasswordlessAuthenticationStepImpl(
            this.identificationService,
            this.stepSession,
            this.service,
            this.userRepository,
            this.relyingPartyId,
            this.stepActions,
            this.stepErrors,
            this.flowAttributes,
            this.logger
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "FidoPasswordlessAuthenticationStepImpl.FidoPasswordlessAuthenticationStepImplBuilder(identificationService="
            + this.identificationService
            + ", stepSession="
            + this.stepSession
            + ", service="
            + this.service
            + ", userRepository="
            + this.userRepository
            + ", relyingPartyId="
            + this.relyingPartyId
            + ", stepActions="
            + this.stepActions
            + ", stepErrors="
            + this.stepErrors
            + ", flowAttributes="
            + this.flowAttributes
            + ", logger="
            + this.logger
            + ")";
      }
   }
}
