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

import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResult;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResultBuilder;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResultFactory;
import com.airlock.iam.authentication.api.domain.service.username.IdentityProvidingService;
import com.airlock.iam.authentication.application.configuration.identification.UserIdentifyingFidoStepConfig;
import com.airlock.iam.authentication.domain.model.fido.FidoPasswordlessAuthenticationStepImpl;
import com.airlock.iam.authentication.domain.model.password.ConditionalFidoSettings;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactor;
import com.airlock.iam.core.domain.model.authentication.method.KnownAuthenticationMethods;
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.FidoUserRepository;
import com.airlock.iam.factor.domain.service.fido.FidoAuthenticationService;
import com.airlock.iam.fido.api.domain.model.FidoRelyingPartyId;
import com.airlock.iam.flow.api.application.configuration.tag.TagConfig;
import com.airlock.iam.flow.api.application.configuration.tag.TagFactory;
import com.airlock.iam.flow.api.domain.model.session.FlowAttributeRepository;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.api.domain.model.tag.Tag;
import com.airlock.iam.flow.shared.domain.model.fido.FidoCheckingStep;
import com.airlock.iam.flow.shared.domain.model.fido.FidoLoginLogger;
import com.airlock.iam.flow.shared.domain.model.identification.UserIdentifyingStepImpl;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.google.common.base.Joiner;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.List;
import java.util.Optional;
import java.util.Set;

@LogicForConfig(UserIdentifyingFidoStepConfig.class)
public final class UserIdentifyingFidoStepImpl implements UserIdentifyingFidoStep {
   private static final String FIDO_CHALLENGE_ATTRIBUTE = "fidoChallenge";
   private final Provider<Optional<UserIdentifyingFidoStepSession>> stepSession;
   private final Provider<ConditionalFidoSettings> conditionalFidoSettings;
   private final Provider<FidoAuthenticationService> fidoAuthService;
   private final Provider<SigningLogger> signingLogger;
   private final Provider<TagFactory> tagFactory;
   private final UserIdentifyingStepImpl userIdentifyingStep;
   private final FidoPasswordlessAuthenticationStepImpl fidoStep;

   @Inject
   public UserIdentifyingFidoStepImpl(
      Provider<IdentityProvidingService> identificationService,
      Provider<SigningLogger> signingLogger,
      Provider<HelpdeskLogger> helpdeskLogger,
      Provider<TagFactory> tagFactory,
      Provider<Optional<UserIdentifyingFidoStepSession>> stepSession,
      Provider<ConditionalFidoSettings> conditionalFidoSettings,
      Provider<FidoAuthenticationService> fidoService,
      Provider<FidoUserRepository> userRepository,
      Provider<FidoRelyingPartyId> relyingPartyId,
      Provider<FidoCheckingStep.FidoStepActions> stepActions,
      Provider<FidoCheckingStep.FidoStepErrors> stepErrors,
      Provider<FlowAttributeRepository> flowAttributes,
      Provider<FidoLoginLogger> fidoLoginLogger
   ) {
      this.userIdentifyingStep = UserIdentifyingStepImpl.builder()
         .identificationService(identificationService)
         .signingLogger(signingLogger)
         .helpdeskLogger(helpdeskLogger)
         .build();
      this.fidoStep = FidoPasswordlessAuthenticationStepImpl.builder()
         .identificationService(identificationService)
         .stepSession(this::stepSession)
         .service(fidoService)
         .userRepository(userRepository)
         .relyingPartyId(relyingPartyId)
         .stepActions(stepActions)
         .stepErrors(stepErrors)
         .flowAttributes(flowAttributes)
         .logger(fidoLoginLogger)
         .build();
      this.signingLogger = signingLogger;
      this.tagFactory = tagFactory;
      this.fidoAuthService = fidoService;
      this.stepSession = stepSession;
      this.conditionalFidoSettings = conditionalFidoSettings;
   }

   @Override
   public AuthenticationStepResult initialize() {
      AuthenticationStepResultBuilder builder = this.stepResultFactory().stay(UserIdentifyingFidoStep.UserIdentifyingFidoStepAction.USERNAME_OR_FIDO_REQUIRED);
      FidoPublicKeyCredentialRequestOptions challenge = this.generateChallengeForPasswordless();
      return this.withFidoChallengeAttribute(builder, challenge);
   }

   public AuthenticationStepResult identifyUser(String providedUsername) {
      this.stepSession().startUsernameIdentification();
      AuthenticationStepResult stepResult = this.toAuthenticationStepResult(this.userIdentifyingStep.identifyUser(providedUsername));
      return stepResult.isFailure()
         ? this.withFidoChallengeAttribute(stepResult.toBuilder(), this.stepSession().getChallenge())
         : this.withOutcomeSpecificSuccessTags(stepResult, ((ConditionalFidoSettings)this.conditionalFidoSettings.get()).primaryTagsOnSuccess());
   }

   @Override
   public AuthenticationStepResult verify(FidoAuthenticatorUnverifiedAssertionResponse authenticatorResponse) {
      this.stepSession().startFidoAuthentication();
      AuthenticationStepResult stepResult = this.fidoStep.verify(authenticatorResponse);
      return stepResult.isFailure()
         ? this.withFidoChallengeAttribute(stepResult.toBuilder(), this.stepSession().getChallenge())
         : this.withOutcomeSpecificSuccessTags(stepResult, ((ConditionalFidoSettings)this.conditionalFidoSettings.get()).tagsOnFidoAuthSuccess());
   }

   @Override
   public AuthenticationStepResult reportClientFailure(FidoClientFailure failure) {
      return this.fidoStep.reportClientFailure(failure);
   }

   public AuthenticationStepResultFactory stepResultFactory() {
      return ((Optional)this.stepSession.get())
         .map(UserIdentifyingFidoStepSession::getActiveStepResultFactory)
         .orElseGet(() -> AuthenticationStepResultFactory.forFactor(KnownAuthenticationFactor.USERNAME));
   }

   @Override
   public AuthenticationMethod getAuthenticationMethod() {
      return ((Optional)this.stepSession.get())
         .filter(session -> session.getActiveStepResult() == UserIdentifyingFidoStep.ActiveFidoUserIdentifyingStepResult.FIDO)
         .map(session -> KnownAuthenticationMethods.FIDO)
         .orElse(KnownAuthenticationMethods.UNSPECIFIED);
   }

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

   @Override
   public String getDescription() {
      return "User Identifying Step with FIDO Authentication";
   }

   private UserIdentifyingFidoStepSession stepSession() {
      return (UserIdentifyingFidoStepSession)((Optional)this.stepSession.get()).orElseThrow();
   }

   private AuthenticationStepResult withFidoChallengeAttribute(
      AuthenticationStepResultBuilder stepResultBuilder, FidoPublicKeyCredentialRequestOptions challenge
   ) {
      return stepResultBuilder.addAdditionalAttribute("fidoChallenge", challenge.asAdditionalAttribute()).build();
   }

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

   private AuthenticationStepResult withOutcomeSpecificSuccessTags(AuthenticationStepResult stepResult, List<TagConfig> successTags) {
      if (stepResult.isSuccess()) {
         Set<Tag> tagsToAdd = ((TagFactory)this.tagFactory.get()).initTags(successTags);
         if (tagsToAdd.isEmpty()) {
            return stepResult;
         }

         ((SigningLogger)this.signingLogger.get()).log("Adding tags to successful step result: " + Joiner.on(", ").join(tagsToAdd));
         return stepResult.toBuilder().addObtainedTags(tagsToAdd).build();
      } else {
         return stepResult;
      }
   }

   private AuthenticationStepResult toAuthenticationStepResult(StepResult stepResult) {
      return this.stepResultFactory().asAuthenticationStepResult(stepResult).build();
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class UserIdentifyingFidoStepImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<IdentityProvidingService> identificationService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SigningLogger> signingLogger;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<HelpdeskLogger> helpdeskLogger;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<TagFactory> tagFactory;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<UserIdentifyingFidoStepSession>> stepSession;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<ConditionalFidoSettings> conditionalFidoSettings;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FidoAuthenticationService> fidoService;
      @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> fidoLoginLogger;

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

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

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder signingLogger(Provider<SigningLogger> signingLogger) {
         this.signingLogger = signingLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder helpdeskLogger(Provider<HelpdeskLogger> helpdeskLogger) {
         this.helpdeskLogger = helpdeskLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder tagFactory(Provider<TagFactory> tagFactory) {
         this.tagFactory = tagFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder stepSession(Provider<Optional<UserIdentifyingFidoStepSession>> stepSession) {
         this.stepSession = stepSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder conditionalFidoSettings(Provider<ConditionalFidoSettings> conditionalFidoSettings) {
         this.conditionalFidoSettings = conditionalFidoSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder fidoService(Provider<FidoAuthenticationService> fidoService) {
         this.fidoService = fidoService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder userRepository(Provider<FidoUserRepository> userRepository) {
         this.userRepository = userRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder relyingPartyId(Provider<FidoRelyingPartyId> relyingPartyId) {
         this.relyingPartyId = relyingPartyId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder stepActions(Provider<FidoCheckingStep.FidoStepActions> stepActions) {
         this.stepActions = stepActions;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder stepErrors(Provider<FidoCheckingStep.FidoStepErrors> stepErrors) {
         this.stepErrors = stepErrors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder flowAttributes(Provider<FlowAttributeRepository> flowAttributes) {
         this.flowAttributes = flowAttributes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder fidoLoginLogger(Provider<FidoLoginLogger> fidoLoginLogger) {
         this.fidoLoginLogger = fidoLoginLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserIdentifyingFidoStepImpl build() {
         return new UserIdentifyingFidoStepImpl(
            this.identificationService,
            this.signingLogger,
            this.helpdeskLogger,
            this.tagFactory,
            this.stepSession,
            this.conditionalFidoSettings,
            this.fidoService,
            this.userRepository,
            this.relyingPartyId,
            this.stepActions,
            this.stepErrors,
            this.flowAttributes,
            this.fidoLoginLogger
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "UserIdentifyingFidoStepImpl.UserIdentifyingFidoStepImplBuilder(identificationService="
            + this.identificationService
            + ", signingLogger="
            + this.signingLogger
            + ", helpdeskLogger="
            + this.helpdeskLogger
            + ", tagFactory="
            + this.tagFactory
            + ", stepSession="
            + this.stepSession
            + ", conditionalFidoSettings="
            + this.conditionalFidoSettings
            + ", fidoService="
            + this.fidoService
            + ", userRepository="
            + this.userRepository
            + ", relyingPartyId="
            + this.relyingPartyId
            + ", stepActions="
            + this.stepActions
            + ", stepErrors="
            + this.stepErrors
            + ", flowAttributes="
            + this.flowAttributes
            + ", fidoLoginLogger="
            + this.fidoLoginLogger
            + ")";
      }
   }
}
