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

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.api.domain.model.validation.GenericError;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.api.domain.model.validation.ValidatorError;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.factor.domain.model.fido.FidoCredentialDisplayName;
import com.airlock.iam.factor.domain.model.fido.FidoUserCredential;
import com.airlock.iam.factor.domain.model.fido.FidoUserCredentialRepository;
import com.airlock.iam.fido.api.domain.model.FidoCredentialId;
import com.airlock.iam.flow.api.domain.model.session.FlowAttributeRepository;
import com.airlock.iam.flow.api.domain.model.step.GenericStepError;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.domain.model.ValidationStepResultData;
import com.airlock.iam.flow.shared.application.configuration.step.FidoCredentialDisplayNameChangeStepConfig;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;

@LogicForConfig(FidoCredentialDisplayNameChangeStepConfig.class)
public final class FidoCredentialDisplayNameChangeStepImpl implements FidoCredentialDisplayNameChangeStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(FidoCredentialDisplayNameChangeStepImpl.class);
   private final Provider<PrincipalInfo> iamUserId;
   private final Provider<FidoUserCredentialRepository> userRepository;
   private final Provider<FlowAttributeRepository> flowAttributes;

   @Inject
   public FidoCredentialDisplayNameChangeStepImpl(
      Provider<PrincipalInfo> user, Provider<FidoUserCredentialRepository> userRepository, Provider<FlowAttributeRepository> flowAttributes
   ) {
      this.iamUserId = user;
      this.userRepository = userRepository;
      this.flowAttributes = flowAttributes;
   }

   @Override
   public StepResult initialize() {
      Optional<FidoUserCredential> fidoUserCredential = this.loadFidoUserCredential();
      if (fidoUserCredential.isEmpty()) {
         throw new FailureException("No credential selected or selected credential no longer exists.");
      }

      this.initializeChange(fidoUserCredential.get().getId(), fidoUserCredential.get().getDisplayName());
      String currentName = this.currentCredentialName();
      LOG.info("Initialized FIDO credential display name change for credential " + fidoUserCredential.get().getId().asString());
      return this.stepResultFactory()
         .stay(FidoCredentialDisplayNameChangeStep.FidoCredentialDisplayNameChangeActions.FIDO_CREDENTIAL_EDIT_POSSIBLE)
         .addAdditionalAttribute("currentFidoCredentialDisplayName", currentName)
         .build();
   }

   @Override
   public StepResult editName(String newName) {
      if (!FidoCredentialDisplayName.isValid(newName)) {
         ValidationResult resultWithField = ValidationResult.invalid(ValidatorError.validatorError("displayName", GenericError.WRONG_FORMAT));
         return this.toFailedRetryResult(resultWithField);
      } else {
         this.updateChange(newName);
         return this.stepResultFactory()
            .stay(FidoCredentialDisplayNameChangeStep.FidoCredentialDisplayNameChangeActions.FIDO_CREDENTIAL_EDIT_POSSIBLE)
            .addAdditionalAttribute("currentFidoCredentialDisplayName", newName)
            .build();
      }
   }

   @Override
   public StepResult validateAndContinue() {
      String currentName = this.currentCredentialName();
      if (!FidoCredentialDisplayName.isValid(currentName)) {
         ValidationResult resultWithField = ValidationResult.invalid(ValidatorError.validatorError("displayName", GenericError.WRONG_FORMAT));
         return this.toFailedRetryResult(resultWithField);
      } else {
         return this.stepResultFactory().success().build();
      }
   }

   @Override
   public StepResult retrieveInformation() {
      String currentName = this.currentCredentialName();
      return this.stepResultFactory()
         .stay(FidoCredentialDisplayNameChangeStep.FidoCredentialDisplayNameChangeActions.FIDO_CREDENTIAL_EDIT_POSSIBLE)
         .resultData(new FidoCredentialEditInformation(currentName))
         .build();
   }

   private Optional<FidoUserCredential> loadFidoUserCredential() {
      return ((FlowAttributeRepository)this.flowAttributes.get())
         .tryGetAttribute(SelectedFidoCredentialFlowAttribute.class)
         .map(SelectedFidoCredentialFlowAttribute::getCredentialId)
         .flatMap(id -> ((FidoUserCredentialRepository)this.userRepository.get()).findById(id, this.userId()));
   }

   private void initializeChange(FidoCredentialId credentialId, FidoCredentialDisplayName existingName) {
      FidoCredentialDisplayNameChangeFlowAttribute attribute = ((FlowAttributeRepository)this.flowAttributes.get())
         .tryGetAttribute(FidoCredentialDisplayNameChangeFlowAttribute.class)
         .orElseGet(() -> new FidoCredentialDisplayNameChangeFlowAttribute(credentialId, existingName.getName()));
      ((FlowAttributeRepository)this.flowAttributes.get()).putOrMergeAttribute(attribute);
   }

   private void updateChange(String newName) {
      FidoCredentialId credentialId = ((FlowAttributeRepository)this.flowAttributes.get())
         .tryGetAttribute(FidoCredentialDisplayNameChangeFlowAttribute.class)
         .map(FidoCredentialDisplayNameChangeFlowAttribute::getCredentialId)
         .orElseThrow();
      ((FlowAttributeRepository)this.flowAttributes.get()).putOrMergeAttribute(new FidoCredentialDisplayNameChangeFlowAttribute(credentialId, newName));
   }

   private String currentCredentialName() {
      return ((FlowAttributeRepository)this.flowAttributes.get())
         .tryGetAttribute(FidoCredentialDisplayNameChangeFlowAttribute.class)
         .map(FidoCredentialDisplayNameChangeFlowAttribute::getNewName)
         .orElse(null);
   }

   private StepResult toFailedRetryResult(ValidationResult resultWithField) {
      String currentName = this.currentCredentialName();
      return this.stepResultFactory()
         .failRetry(
            GenericStepError.VALIDATION_FAILED, FidoCredentialDisplayNameChangeStep.FidoCredentialDisplayNameChangeActions.FIDO_CREDENTIAL_EDIT_POSSIBLE
         )
         .resultData(new ValidationStepResultData(resultWithField))
         .addAdditionalAttribute("currentFidoCredentialDisplayName", currentName)
         .build();
   }

   private UserId userId() {
      return ((PrincipalInfo)this.iamUserId.get()).getKnownIdentity().map(UserIdentity::id).orElseThrow(() -> new FailureException("User must be known"));
   }

   @Override
   public String getDescription() {
      return "FIDO Credential Display Name Change Step";
   }
}
