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

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.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.base.misc.json.JsonCoding;
import com.airlock.iam.base.misc.result.Result;
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.authentication.method.KnownAuthenticationMethods;
import com.airlock.iam.factor.domain.model.fido.FidoApprovalData;
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.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.application.configuration.step.CommonFidoCheckingStepConfig;
import com.airlock.iam.flow.shared.domain.model.attributes.AuthTokenIdAttribute;
import com.airlock.iam.flow.shared.domain.model.attributes.AuthTokenIdFlowSessionAttribute;
import com.airlock.iam.log.api.signing.SigningLogger;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Map;
import java.util.Optional;
import lombok.NonNull;

@LogicForConfig(CommonFidoCheckingStepConfig.class)
public final class FidoCheckingStepImpl implements FidoCheckingStepWithRetrieve {
   private static final String FIDO_CHALLENGE_ATTRIBUTE = "fidoChallenge";
   private final Provider<FidoCheckingStepSession> stepSession;
   private final Provider<FidoAuthenticationService> service;
   private final Provider<FidoUserRepository> userRepository;
   private final Provider<PrincipalInfo> user;
   private final Provider<FidoRelyingPartyId> relyingPartyId;
   private final Provider<FidoCheckingStep.FidoStepActions> stepActions;
   private final Provider<FidoCheckingStep.FidoStepErrors> stepErrors;
   private final Provider<FidoLoginLogger> logger;
   private final Provider<FlowAttributeRepository> flowAttributes;
   private final Provider<Optional<FidoApprovalData>> approvalData;
   private final Provider<FidoCheckingStepImpl.FidoTransactionApprovalStepSettings> approvalStepSettings;
   private final SigningLogger signingLogger;

   @Inject
   public FidoCheckingStepImpl(
      @NonNull Provider<FidoCheckingStepSession> stepSession,
      @NonNull Provider<FidoUserRepository> userRepository,
      @NonNull Provider<FidoAuthenticationService> service,
      @NonNull Provider<PrincipalInfo> user,
      @NonNull Provider<FidoRelyingPartyId> relyingPartyId,
      @NonNull Provider<FidoCheckingStep.FidoStepActions> stepActions,
      @NonNull Provider<FidoCheckingStep.FidoStepErrors> stepErrors,
      @NonNull Provider<FidoLoginLogger> logger,
      @NonNull Provider<FlowAttributeRepository> flowAttributes,
      @NonNull Provider<Optional<FidoApprovalData>> approvalData,
      @NonNull Provider<FidoCheckingStepImpl.FidoTransactionApprovalStepSettings> settings,
      @NonNull SigningLogger signingLogger
   ) {
      if (stepSession == null) {
         throw new NullPointerException("stepSession is marked non-null but is null");
      }

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

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

      if (user == null) {
         throw new NullPointerException("user 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 (logger == null) {
         throw new NullPointerException("logger is marked non-null but is null");
      }

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

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

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

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

      this.stepSession = stepSession;
      this.userRepository = userRepository;
      this.service = service;
      this.user = user;
      this.relyingPartyId = relyingPartyId;
      this.stepActions = stepActions;
      this.stepErrors = stepErrors;
      this.logger = logger;
      this.flowAttributes = flowAttributes;
      this.approvalData = approvalData;
      this.approvalStepSettings = settings;
      this.signingLogger = signingLogger;
   }

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

   @Override
   public AuthenticationStepResult initialize() {
      if (!((FidoAuthenticationService)this.service.get()).hasEligibleFidoCredentials(this.userId())) {
         return this.stepResultFactory().fail(AuthenticationFlowStep.GeneralAuthenticationStepError.NO_VALID_TOKEN).build();
      }

      FidoPublicKeyCredentialRequestOptions challenge = this.generateChallenge();
      return this.stepResultFactory()
         .stay(((FidoCheckingStep.FidoStepActions)this.stepActions.get()).challengeRetrievalRequired())
         .addAdditionalAttribute("fidoChallenge", challenge.asAdditionalAttribute())
         .build();
   }

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

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

   private Optional<FidoAuthTokenId> fidoCredentialFromAuthTokenId() {
      return ((FlowAttributeRepository)this.flowAttributes.get())
         .tryGetAttribute(AuthTokenIdAttribute.class)
         .map(AuthTokenIdAttribute::getAuthTokenId)
         .flatMap(FidoAuthTokenId::parse);
   }

   private FidoUserPrincipal retrieveFidoUser() {
      FidoUserPrincipal fidoUser = ((FidoUserRepository)this.userRepository.get())
         .findBy((FidoRelyingPartyId)this.relyingPartyId.get(), this.userId())
         .orElseThrow(
            () -> new FailureException(
               String.format(
                  "No existing FIDO user with relying party ID %s and IAM user %s", ((FidoRelyingPartyId)this.relyingPartyId.get()).getId(), this.userId()
               )
            )
         );
      ((FidoCheckingStepSession)this.stepSession.get()).saveUser(fidoUser);
      return fidoUser;
   }

   private FidoPublicKeyCredentialRequestOptions generateChallenge() {
      FidoUserPrincipal fidoUser = this.retrieveFidoUser();
      FidoPublicKeyCredentialRequestOptions challenge = ((Optional)this.approvalData.get())
         .<FidoPublicKeyCredentialRequestOptions>map(
            data -> ((FidoAuthenticationService)this.service.get()).generateChallengeWithApprovalData(fidoUser, data, this.fidoCredentialFromAuthTokenId())
         )
         .orElse(((FidoAuthenticationService)this.service.get()).generateChallenge(fidoUser));
      ((FidoCheckingStepSession)this.stepSession.get()).saveChallenge(challenge);
      return challenge;
   }

   @Override
   public AuthenticationStepResult verify(FidoAuthenticatorUnverifiedAssertionResponse authenticatorResponse) {
      FidoUserPrincipal fidoUser = ((FidoCheckingStepSession)this.stepSession.get()).getFidoUser().orElseThrow();
      Result<FidoRegisteredCredential, FidoRelyingParty.FidoAuthenticationVerificationErrorCode> result = ((FidoAuthenticationService)this.service.get())
         .verifyFidoResponse(((FidoCheckingStepSession)this.stepSession.get()).getChallenge(), fidoUser, authenticatorResponse);
      if (!result.isFailure() && this.shouldLogApprovalParameters()) {
         this.logCryptographicallyVerifiableFidoApprovalData(authenticatorResponse, result);
      }

      if (result.isFailure()) {
         ((FidoLoginLogger)this.logger.get()).logAuthenticationFailure(fidoUser, authenticatorResponse);
         return this.stepResultFactory()
            .fail(FidoStepErrorMapper.mapError(result.getFailure(), (FidoCheckingStep.FidoStepErrors)this.stepErrors.get()))
            .build();
      } else {
         ((FidoLoginLogger)this.logger.get()).logAuthenticationSuccess(result.get());
         FidoRegisteredCredential fidoRegisteredCredential = result.get();
         ((FlowAttributeRepository)this.flowAttributes.get())
            .putOrMergeAttribute(
               AuthTokenIdFlowSessionAttribute.authTokenIdFlowSessionAttribute(
                  FidoAuthTokenId.fidoAuthTokenId(fidoRegisteredCredential.getId()).formatAsString()
               )
            );
         return this.stepResultFactory().success().build();
      }
   }

   @Override
   public AuthenticationStepResult reportClientFailure(FidoClientFailure failure) {
      ((FidoLoginLogger)this.logger.get()).logClientFailure(this.userId(), failure.message());
      return this.stepResultFactory()
         .fail(FidoStepErrorMapper.mapError(failure.reason(), (FidoCheckingStep.FidoStepErrors)this.stepErrors.get()))
         .isFailedFactor(false)
         .build();
   }

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

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

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

   private boolean shouldLogApprovalParameters() {
      return ((FidoCheckingStepImpl.FidoTransactionApprovalStepSettings)this.approvalStepSettings.get()).verificationLogging
         && ((FidoCheckingStepSession)this.stepSession.get()).getChallenge().getChallenge().getNonce().isPresent();
   }

   private void logCryptographicallyVerifiableFidoApprovalData(
      FidoAuthenticatorUnverifiedAssertionResponse authenticatorResponse,
      Result<FidoRegisteredCredential, FidoRelyingParty.FidoAuthenticationVerificationErrorCode> result
   ) {
      Map<String, String> map = Map.of(
         "N",
         ((FidoCheckingStepSession)this.stepSession.get()).getChallenge().getChallenge().getNonce().orElseThrow(),
         "T",
         ((FidoApprovalData)((Optional)this.approvalData.get()).orElseThrow()).getApprovalDataAsJson(),
         "CAD",
         authenticatorResponse.getAuthenticatorData(),
         "CCD",
         authenticatorResponse.getClientDataJson(),
         "S",
         authenticatorResponse.getSignature(),
         "P",
         result.get().getPublicKey().base64Url()
      );
      this.signingLogger.log(String.format("FIDO Dynamic Linking Verification Data: %s", JsonCoding.toJson(map)));
   }

   public static final class FidoTransactionApprovalStepSettings {
      private final boolean verificationLogging;

      @SuppressFBWarnings(justification = "generated code")
      FidoTransactionApprovalStepSettings(boolean verificationLogging) {
         this.verificationLogging = verificationLogging;
      }

      @SuppressFBWarnings(justification = "generated code")
      public static FidoCheckingStepImpl.FidoTransactionApprovalStepSettings.FidoTransactionApprovalStepSettingsBuilder builder() {
         return new FidoCheckingStepImpl.FidoTransactionApprovalStepSettings.FidoTransactionApprovalStepSettingsBuilder();
      }

      @SuppressFBWarnings(justification = "generated code")
      public boolean isVerificationLogging() {
         return this.verificationLogging;
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public boolean equals(Object o) {
         if (o == this) {
            return true;
         } else {
            return !(o instanceof FidoCheckingStepImpl.FidoTransactionApprovalStepSettings other)
               ? false
               : this.verificationLogging == other.verificationLogging;
         }
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public int hashCode() {
         int PRIME = 59;
         int result = 1;
         return result * 59 + (this.verificationLogging ? 79 : 97);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "FidoCheckingStepImpl.FidoTransactionApprovalStepSettings(verificationLogging=" + this.isVerificationLogging() + ")";
      }

      @SuppressFBWarnings(justification = "generated code")
      public static class FidoTransactionApprovalStepSettingsBuilder {
         @SuppressFBWarnings(justification = "generated code")
         private boolean verificationLogging;

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

         @SuppressFBWarnings(justification = "generated code")
         public FidoCheckingStepImpl.FidoTransactionApprovalStepSettings.FidoTransactionApprovalStepSettingsBuilder verificationLogging(
            boolean verificationLogging
         ) {
            this.verificationLogging = verificationLogging;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public FidoCheckingStepImpl.FidoTransactionApprovalStepSettings build() {
            return new FidoCheckingStepImpl.FidoTransactionApprovalStepSettings(this.verificationLogging);
         }

         @SuppressFBWarnings(justification = "generated code")
         @Override
         public String toString() {
            return "FidoCheckingStepImpl.FidoTransactionApprovalStepSettings.FidoTransactionApprovalStepSettingsBuilder(verificationLogging="
               + this.verificationLogging
               + ")";
         }
      }
   }
}
