package com.airlock.iam.factor.infrastructure.service.fido;

import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorUnverifiedAttestationResponse;
import com.airlock.iam.factor.domain.model.fido.FidoPublicKeyCredentialCreationOptions;
import com.airlock.iam.fido.api.domain.model.FidoPublicKeyCredentialId;
import com.yubico.webauthn.FinishRegistrationOptions;
import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
import com.yubico.webauthn.data.AuthenticatorSelectionCriteria;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.RegistrationExtensionInputs;
import com.yubico.webauthn.data.RelyingPartyIdentity;
import com.yubico.webauthn.data.ResidentKeyRequirement;
import com.yubico.webauthn.data.UserIdentity;
import com.yubico.webauthn.data.exception.Base64UrlException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.NonNull;

final class RegistrationYubicoConverter {
   private final FidoPublicKeyCredentialCreationOptions creationOptions;
   private final ByteArray challenge;
   private final ByteArray userHandle;
   private final ByteArray publicKeyCredentialId;
   private final ByteArray attestationObject;
   private final ByteArray clientDataJson;

   private RegistrationYubicoConverter(
      @NonNull FidoPublicKeyCredentialCreationOptions creationOptions, @NonNull FidoAuthenticatorUnverifiedAttestationResponse response
   ) {
      if (creationOptions == null) {
         throw new NullPointerException("creationOptions is marked non-null but is null");
      }

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

      this.creationOptions = creationOptions;
      this.challenge = YubicoDecoder.decode(creationOptions.getChallenge());
      this.userHandle = YubicoDecoder.decode(creationOptions.getFidoUser().getUserHandle());
      this.publicKeyCredentialId = YubicoDecoder.decodeBase64Url(response.publicKeyCredentialId());
      this.attestationObject = YubicoDecoder.decodeBase64Url(response.attestationObject());
      this.clientDataJson = YubicoDecoder.decodeClientDataJson(response.clientDataJson());
   }

   static RegistrationYubicoConverter registrationYubicoConverter(
      FidoPublicKeyCredentialCreationOptions challenge, FidoAuthenticatorUnverifiedAttestationResponse response
   ) {
      return new RegistrationYubicoConverter(challenge, response);
   }

   RelyingPartyIdentity relyingPartyIdentity() {
      return RelyingPartyIdentity.builder()
         .id(YubicoDecoder.relyingPartyId(this.creationOptions.getFidoUser().getRelyingPartyId()))
         .name(YubicoDecoder.relyingPartyName(this.creationOptions.getRegistrationSettings().getRelyingPartyName()))
         .build();
   }

   PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions() {
      return PublicKeyCredentialCreationOptions.builder()
         .rp(this.relyingPartyIdentity())
         .user(this.userIdentity())
         .challenge(this.challenge)
         .pubKeyCredParams(YubicoDecoder.publicKeyCredentialParameters(this.creationOptions.getRegistrationSettings().getPreferredAlgorithms()))
         .authenticatorSelection(this.authenticatorSelection())
         .attestation(YubicoDecoder.attestationConveyancePreference(this.creationOptions.getRegistrationSettings().getAttestationType()))
         .excludeCredentials(this.publicKeyCredentialDescriptors(this.creationOptions.getExcludedCredentialIds()))
         .extensions(RegistrationExtensionInputs.builder().build())
         .timeout(YubicoDecoder.timeoutInMillis(this.creationOptions.getRegistrationSettings().getRegistrationTimeout()))
         .build();
   }

   UserIdentity userIdentity() {
      return UserIdentity.builder()
         .name(YubicoDecoder.credentialName(this.creationOptions.getRegistrationSettings().getCredentialName()))
         .displayName(YubicoDecoder.credentialDisplayName(this.creationOptions.getCredentialDisplayName()))
         .id(this.userHandle)
         .build();
   }

   AuthenticatorSelectionCriteria authenticatorSelection() {
      return AuthenticatorSelectionCriteria.builder()
         .residentKey(this.residentKeyRequirement())
         .userVerification(YubicoDecoder.userVerificationRequirement(this.creationOptions.getRegistrationSettings().getUserVerification()))
         .authenticatorAttachment(YubicoDecoder.authenticatorAttachment(this.creationOptions.getRegistrationSettings().getAuthenticatorType()))
         .build();
   }

   private ResidentKeyRequirement residentKeyRequirement() {
      return ResidentKeyRequirement.valueOf(this.creationOptions.getRegistrationSettings().getResidentKey().name());
   }

   Set<PublicKeyCredentialDescriptor> publicKeyCredentialDescriptors(List<FidoPublicKeyCredentialId> credentialIds) {
      return credentialIds.stream()
         .map(id -> PublicKeyCredentialDescriptor.builder().id(YubicoDecoder.decode(id)).transports(Optional.empty()).build())
         .collect(Collectors.toCollection(LinkedHashSet::new));
   }

   FinishRegistrationOptions registrationOptions() throws IOException, Base64UrlException {
      return FinishRegistrationOptions.builder().request(this.publicKeyCredentialCreationOptions()).response(this.publicKeyCredential()).build();
   }

   PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> publicKeyCredential() throws Base64UrlException, IOException {
      return PublicKeyCredential.builder()
         .id(this.publicKeyCredentialId)
         .response(this.authenticatorAttestationResponse())
         .clientExtensionResults(ClientRegistrationExtensionOutputs.builder().build())
         .build();
   }

   AuthenticatorAttestationResponse authenticatorAttestationResponse() throws IOException, Base64UrlException {
      return AuthenticatorAttestationResponse.builder().attestationObject(this.attestationObject).clientDataJSON(this.clientDataJson).build();
   }

   @SuppressFBWarnings(justification = "generated code")
   ByteArray getAttestationObject() {
      return this.attestationObject;
   }
}
