package com.airlock.iam.flow.shared.infrastructure.service.fido;

import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.core.domain.model.token.TokenUsage;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.factor.domain.model.fido.FidoAttestationVerifier;
import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorInfo;
import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorMetadataRegistry;
import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorModel;
import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorModelId;
import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorUnverifiedAttestationResponse;
import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorVerifiedAttestationResponse;
import com.airlock.iam.factor.domain.model.fido.FidoChallengeGenerator;
import com.airlock.iam.factor.domain.model.fido.FidoCredentialDisplayName;
import com.airlock.iam.factor.domain.model.fido.FidoCredentialRegistrationSettings;
import com.airlock.iam.factor.domain.model.fido.FidoCredentialRepository;
import com.airlock.iam.factor.domain.model.fido.FidoPublicKeyCredentialCreationOptions;
import com.airlock.iam.factor.domain.model.fido.FidoRegisteredCredential;
import com.airlock.iam.factor.domain.model.fido.FidoRegistrationCredential;
import com.airlock.iam.factor.domain.model.fido.FidoRelyingParty;
import com.airlock.iam.factor.domain.model.fido.FidoSignatureCounter;
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.FidoRegistrationService;
import com.airlock.iam.fido.api.domain.model.FidoCredentialId;
import com.airlock.iam.fido.api.domain.model.FidoPublicKeyCredentialId;
import com.airlock.iam.fido.api.domain.model.FidoRelyingPartyId;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import lombok.NonNull;

public final class FidoRegistrationServiceImpl implements FidoRegistrationService {
   private static final ELogger LOG = ELoggerFactory.getLogger(FidoRegistrationServiceImpl.class);
   public static final String DEFAULT_DISPLAY_NAME = "-";
   private final Provider<FidoUserRepository> userRepository;
   private final Provider<FidoCredentialRepository> credentialRepository;
   private final Provider<FidoCredentialRegistrationSettings> registrationSettings;
   private final Provider<FidoRelyingParty> relyingParty;
   private final FidoChallengeGenerator challengeGenerator;
   private final FidoAttestationVerifier attestationVerifier;
   private final TimeProvider timeProvider;
   private final Provider<FidoAuthenticatorMetadataRegistry> metadataRegistry;

   @Inject
   public FidoRegistrationServiceImpl(
      @NonNull Provider<FidoUserRepository> userRepository,
      @NonNull Provider<FidoCredentialRepository> credentialRepository,
      @NonNull Provider<FidoCredentialRegistrationSettings> registrationSettings,
      @NonNull Provider<FidoRelyingParty> relyingParty,
      @NonNull FidoChallengeGenerator challengeGenerator,
      @NonNull FidoAttestationVerifier attestationVerifier,
      @NonNull TimeProvider timeProvider,
      @NonNull Provider<FidoAuthenticatorMetadataRegistry> metadataRegistry
   ) {
      if (userRepository == null) {
         throw new NullPointerException("userRepository is marked non-null but is null");
      }

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

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

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

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

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

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

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

      this.userRepository = userRepository;
      this.credentialRepository = credentialRepository;
      this.registrationSettings = registrationSettings;
      this.relyingParty = relyingParty;
      this.challengeGenerator = challengeGenerator;
      this.attestationVerifier = attestationVerifier;
      this.timeProvider = timeProvider;
      this.metadataRegistry = metadataRegistry;
   }

   @Override
   public FidoPublicKeyCredentialCreationOptions generateChallenge(
      FidoRelyingPartyId relyingPartyId, UserId iamUserId, FidoCredentialDisplayName credentialDisplayName
   ) {
      FidoUserPrincipal fidoUser = this.retrieveOrGenerate(relyingPartyId, iamUserId);
      return FidoPublicKeyCredentialCreationOptions.builder()
         .registrationSettings((FidoCredentialRegistrationSettings)this.registrationSettings.get())
         .fidoUser(fidoUser)
         .credentialDisplayName(credentialDisplayName)
         .challenge(this.challengeGenerator.generateRandom())
         .excludedCredentialIds(this.credentialIdsForUser(fidoUser))
         .createdAt(this.timeProvider.getCurrentInstant())
         .build();
   }

   @Override
   public boolean isAutoGenerateDisplayNameEnabled() {
      return ((FidoCredentialRegistrationSettings)this.registrationSettings.get()).isAutoGenerateDisplayName();
   }

   private FidoUserPrincipal retrieveOrGenerate(FidoRelyingPartyId relyingPartyId, UserId iamUserId) {
      return ((FidoUserRepository)this.userRepository.get())
         .findBy(relyingPartyId, iamUserId)
         .orElseGet(() -> this.generateFidoUser(relyingPartyId, iamUserId));
   }

   private FidoUserPrincipal generateFidoUser(FidoRelyingPartyId relyingPartyId, UserId iamUserId) {
      FidoUserPrincipal newFidoUser = FidoUserPrincipal.newFidoUser(relyingPartyId, iamUserId);
      ((FidoUserRepository)this.userRepository.get()).insert(newFidoUser);
      LOG.info(String.format("Created new FIDO user '%s'", newFidoUser));
      return newFidoUser;
   }

   private List<FidoPublicKeyCredentialId> credentialIdsForUser(FidoUserPrincipal fidoUser) {
      return ((FidoCredentialRegistrationSettings)this.registrationSettings.get()).isPreventDoubleRegistration()
         ? ((FidoCredentialRepository)this.credentialRepository.get()).credentialIdsRegisteredTo(fidoUser)
         : Collections.emptyList();
   }

   @Override
   public Result<FidoRegistrationCredential, FidoRelyingParty.FidoRegistrationVerificationErrorCode> verifyAuthenticatorAttestation(
      FidoPublicKeyCredentialCreationOptions challenge, FidoAuthenticatorUnverifiedAttestationResponse response
   ) {
      if (!challenge.isValidAt(this.timeProvider.getCurrentInstant())) {
         LOG.info(String.format("Challenge for registration of a FIDO credential for user '%s' is no longer valid", challenge.getFidoUser()));
         return Result.failure(FidoRelyingParty.FidoRegistrationVerificationErrorCode.REGISTRATION_TIMEOUT);
      } else {
         Result<FidoAuthenticatorVerifiedAttestationResponse, FidoRelyingParty.FidoRegistrationVerificationError> verifiedAttestationResponse = ((FidoRelyingParty)this.relyingParty
               .get())
            .verifyRegistration(challenge, response, this.attestationVerifier);
         if (verifiedAttestationResponse.isPresent()) {
            FidoRegistrationCredential fidoCredential = this.newFidoCredential(challenge, verifiedAttestationResponse.get());
            LOG.info(
               String.format(
                  "User '%s' successfully registered new FIDO credential with ID '%s'",
                  challenge.getFidoUser().getIamUsername().orElseThrow().getValue(),
                  fidoCredential.getId()
               )
            );
            return Result.result(fidoCredential);
         } else {
            FidoRelyingParty.FidoRegistrationVerificationError error = verifiedAttestationResponse.getFailure();
            LOG.info(
               String.format(
                  "Registration of FIDO credential for user '%s' failed. Cause: '%s'",
                  challenge.getFidoUser().getIamUsername().orElseThrow().getValue(),
                  error.getMessage()
               )
            );
            return Result.failure(error.getErrorCode());
         }
      }
   }

   private FidoRegistrationCredential newFidoCredential(
      FidoPublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions, FidoAuthenticatorVerifiedAttestationResponse verifiedAttestationResponse
   ) {
      return FidoRegistrationCredential.builder()
         .id(FidoCredentialId.newCredentialId())
         .userHandle(publicKeyCredentialCreationOptions.getFidoUser().getUserHandle())
         .name(publicKeyCredentialCreationOptions.getRegistrationSettings().getCredentialName())
         .displayName(
            this.isAutoGenerateDisplayNameEnabled()
               ? this.determineDisplayName(verifiedAttestationResponse.attestationObject().aaguid())
               : publicKeyCredentialCreationOptions.getCredentialDisplayName()
         )
         .publicKeyCredentialId(verifiedAttestationResponse.publicKeyCredentialId())
         .publicKey(verifiedAttestationResponse.publicKey())
         .residentRequested(publicKeyCredentialCreationOptions.getRegistrationSettings().effectiveRequireResidentKey())
         .residentKey(publicKeyCredentialCreationOptions.getRegistrationSettings().getResidentKey())
         .attestationObject(verifiedAttestationResponse.attestationObject())
         .registeredAt(this.timeProvider.getCurrentInstant())
         .signatureCounter(FidoSignatureCounter.zero())
         .usage(TokenUsage.notYetUsed())
         .locked(false)
         .transports(verifiedAttestationResponse.transports())
         .build();
   }

   private FidoCredentialDisplayName determineDisplayName(FidoAuthenticatorModelId aaguid) {
      return this.mapAuthenticatorInfo(aaguid)
         .flatMap(FidoAuthenticatorInfo::getModel)
         .map(FidoAuthenticatorModel::getModel)
         .map(FidoCredentialDisplayName::credentialDisplayName)
         .orElse(FidoCredentialDisplayName.credentialDisplayName("-"));
   }

   private Optional<FidoAuthenticatorInfo> mapAuthenticatorInfo(FidoAuthenticatorModelId aaguid) {
      return aaguid.isNotEmpty()
         ? Optional.of(new FidoAuthenticatorInfo(aaguid, ((FidoAuthenticatorMetadataRegistry)this.metadataRegistry.get()).findModelById(aaguid)))
         : Optional.empty();
   }

   @Override
   public FidoRegisteredCredential register(FidoRegistrationCredential credential) {
      return ((FidoCredentialRepository)this.credentialRepository.get()).insert(credential);
   }
}
