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

import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorUnverifiedAssertionResponse;
import com.airlock.iam.factor.domain.model.fido.FidoPublicKeyCredentialRequestOptions;
import com.airlock.iam.factor.domain.model.fido.user.FidoUser;
import com.airlock.iam.fido.api.domain.model.FidoPublicKeyCredentialId;
import com.yubico.webauthn.AssertionRequest;
import com.yubico.webauthn.FinishAssertionOptions;
import com.yubico.webauthn.AssertionRequest.AssertionRequestBuilder;
import com.yubico.webauthn.data.AssertionExtensionInputs;
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions.PublicKeyCredentialRequestOptionsBuilder;
import com.yubico.webauthn.data.exception.Base64UrlException;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.NonNull;

final class AuthenticationYubicoConverter {
   private final FidoPublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions;
   private final FidoUser fidoUser;
   private final ByteArray challenge;
   private final Optional<ByteArray> userHandle;
   private final ByteArray publicKeyCredentialId;
   private final ByteArray authenticatorData;
   private final ByteArray signature;
   private final ByteArray clientDataJson;

   private AuthenticationYubicoConverter(
      @NonNull FidoPublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions,
      @NonNull FidoUser fidoUser,
      @NonNull FidoAuthenticatorUnverifiedAssertionResponse response
   ) {
      if (publicKeyCredentialRequestOptions == null) {
         throw new NullPointerException("publicKeyCredentialRequestOptions is marked non-null but is null");
      }

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

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

      this.publicKeyCredentialRequestOptions = publicKeyCredentialRequestOptions;
      this.fidoUser = fidoUser;
      this.challenge = YubicoDecoder.decode(publicKeyCredentialRequestOptions.getChallenge());
      this.userHandle = response.getUserHandle().map(YubicoDecoder::decodeBase64Url);
      this.publicKeyCredentialId = YubicoDecoder.decodeBase64Url(response.getPublicKeyCredentialId());
      this.authenticatorData = YubicoDecoder.decodeBase64Url(response.getAuthenticatorData());
      this.signature = YubicoDecoder.decodeBase64Url(response.getSignature());
      this.clientDataJson = YubicoDecoder.decodeClientDataJson(response.getClientDataJson());
   }

   static AuthenticationYubicoConverter authenticationYubicoConverter(
      FidoPublicKeyCredentialRequestOptions challenge, FidoUser fidoUser, FidoAuthenticatorUnverifiedAssertionResponse response
   ) {
      return new AuthenticationYubicoConverter(challenge, fidoUser, response);
   }

   private PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions() {
      PublicKeyCredentialRequestOptionsBuilder builder = PublicKeyCredentialRequestOptions.builder()
         .challenge(this.challenge)
         .rpId(this.publicKeyCredentialRequestOptions.getRelyingPartyId().getId())
         .timeout(YubicoDecoder.timeoutInMillis(this.publicKeyCredentialRequestOptions.getAuthenticationSettings().authenticationTimeout()))
         .userVerification(YubicoDecoder.userVerificationRequirement(this.publicKeyCredentialRequestOptions.getAuthenticationSettings().userVerification()))
         .extensions(AssertionExtensionInputs.builder().build());
      this.publicKeyCredentialRequestOptions
         .getAllowedCredentialIds()
         .filter(credentialIds -> !credentialIds.isEmpty())
         .ifPresent(credentialIds -> builder.allowCredentials(this.publicKeyCredentialDescriptors((List<FidoPublicKeyCredentialId>)credentialIds)));
      return builder.build();
   }

   private List<PublicKeyCredentialDescriptor> publicKeyCredentialDescriptors(List<FidoPublicKeyCredentialId> credentialIds) {
      return credentialIds.stream()
         .map(id -> PublicKeyCredentialDescriptor.builder().id(YubicoDecoder.decode(id)).transports(Optional.empty()).build())
         .collect(Collectors.toList());
   }

   FinishAssertionOptions assertionOptions() throws IOException, Base64UrlException {
      return FinishAssertionOptions.builder().request(this.assertionRequest()).response(this.publicKeyCredential()).build();
   }

   private AssertionRequest assertionRequest() {
      AssertionRequestBuilder builder = AssertionRequest.builder().publicKeyCredentialRequestOptions(this.publicKeyCredentialRequestOptions());
      this.fidoUser.getIamUsername().ifPresent(userId -> builder.username(userId.getValue()));
      return builder.build();
   }

   PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> publicKeyCredential() throws Base64UrlException, IOException {
      return PublicKeyCredential.builder()
         .id(this.publicKeyCredentialId)
         .response(this.authenticatorAssertionResponse())
         .clientExtensionResults(ClientAssertionExtensionOutputs.builder().build())
         .build();
   }

   private AuthenticatorAssertionResponse authenticatorAssertionResponse() throws IOException, Base64UrlException {
      return AuthenticatorAssertionResponse.builder()
         .authenticatorData(this.authenticatorData)
         .clientDataJSON(this.clientDataJson)
         .signature(this.signature)
         .userHandle(this.userHandle)
         .build();
   }
}
