import {
  CreateCredentialResult,
  AssertCredentialResult,
} from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils";

import {
  InsecureAssertCredentialParams,
  InsecureCreateCredentialParams,
} from "../content/messaging/message";

export class WebauthnUtils {
  static mapCredentialCreationOptions(
    options: CredentialCreationOptions,
    fallbackSupported: boolean,
  ): InsecureCreateCredentialParams {
    const keyOptions = options.publicKey;

    if (keyOptions == undefined) {
      throw new Error("Public-key options not found");
    }

    return {
      attestation: keyOptions.attestation,
      authenticatorSelection: {
        requireResidentKey: keyOptions.authenticatorSelection?.requireResidentKey,
        residentKey: keyOptions.authenticatorSelection?.residentKey,
        userVerification: keyOptions.authenticatorSelection?.userVerification,
      },
      challenge: Fido2Utils.arrayToString(
        Fido2Utils.bufferSourceToUint8Array(keyOptions.challenge),
      ),
      excludeCredentials: keyOptions.excludeCredentials?.map((credential) => ({
        id: Fido2Utils.arrayToString(Fido2Utils.bufferSourceToUint8Array(credential.id)),
        transports: credential.transports,
        type: credential.type,
      })),
      extensions: {
        credProps: keyOptions.extensions?.credProps,
      },
      pubKeyCredParams: keyOptions.pubKeyCredParams
        .map((params) => ({
          // Fix for spec-deviation: Sites using KeycloakJS send `kp.alg` as a string
          alg: Number(params.alg),
          type: params.type,
        }))
        .filter((params) => !isNaN(params.alg)),
      rp: {
        id: keyOptions.rp.id,
        name: keyOptions.rp.name,
      },
      user: {
        id: Fido2Utils.arrayToString(Fido2Utils.bufferSourceToUint8Array(keyOptions.user.id)),
        displayName: keyOptions.user.displayName,
        name: keyOptions.user.name,
      },
      timeout: keyOptions.timeout,
      fallbackSupported,
    };
  }

  static mapCredentialRegistrationResult(result: CreateCredentialResult): PublicKeyCredential {
    const credential = {
      id: result.credentialId,
      rawId: Fido2Utils.stringToArray(result.credentialId).buffer,
      type: "public-key",
      authenticatorAttachment: "platform",
      response: {
        clientDataJSON: Fido2Utils.stringToArray(result.clientDataJSON).buffer,
        attestationObject: Fido2Utils.stringToArray(result.attestationObject).buffer,

        getAuthenticatorData(): ArrayBuffer {
          return Fido2Utils.stringToArray(result.authData).buffer;
        },

        getPublicKey(): ArrayBuffer {
          return Fido2Utils.stringToArray(result.publicKey).buffer;
        },

        getPublicKeyAlgorithm(): number {
          return result.publicKeyAlgorithm;
        },

        getTransports(): string[] {
          return result.transports;
        },
      } as AuthenticatorAttestationResponse,
      getClientExtensionResults: () => ({
        credProps: result.extensions.credProps,
      }),
      toJSON: () => Fido2Utils.createResultToJson(result),
    } as PublicKeyCredential;

    // Modify prototype chains to fix `instanceof` calls.
    // This makes these objects indistinguishable from the native classes.
    // Unfortunately PublicKeyCredential does not have a javascript constructor so `extends` does not work here.
    Object.setPrototypeOf(credential.response, AuthenticatorAttestationResponse.prototype);
    Object.setPrototypeOf(credential, PublicKeyCredential.prototype);

    return credential;
  }

  static mapCredentialRequestOptions(
    options: CredentialRequestOptions,
    fallbackSupported: boolean,
  ): InsecureAssertCredentialParams {
    const keyOptions = options.publicKey;

    if (keyOptions == undefined) {
      throw new Error("Public-key options not found");
    }

    return {
      allowedCredentials:
        keyOptions.allowCredentials?.map((c) => ({
          id: Fido2Utils.arrayToString(Fido2Utils.bufferSourceToUint8Array(c.id)),
          transports: c.transports ?? [],
        })) ?? [],
      challenge: Fido2Utils.arrayToString(
        Fido2Utils.bufferSourceToUint8Array(keyOptions.challenge),
      ),
      rpId: keyOptions.rpId,
      userVerification: keyOptions.userVerification,
      timeout: keyOptions.timeout,
      mediation: options.mediation,
      fallbackSupported,
    };
  }

  static mapCredentialAssertResult(result: AssertCredentialResult): PublicKeyCredential {
    const credential = {
      id: result.credentialId,
      rawId: Fido2Utils.stringToArray(result.credentialId).buffer,
      type: "public-key",
      response: {
        authenticatorData: Fido2Utils.stringToArray(result.authenticatorData).buffer,
        clientDataJSON: Fido2Utils.stringToArray(result.clientDataJSON).buffer,
        signature: Fido2Utils.stringToArray(result.signature).buffer,
        userHandle: Fido2Utils.stringToArray(result.userHandle).buffer,
      } as AuthenticatorAssertionResponse,
      getClientExtensionResults: () => ({}),
      authenticatorAttachment: "platform",
      toJSON: () => Fido2Utils.getResultToJson(result),
    } as PublicKeyCredential;

    // Modify prototype chains to fix `instanceof` calls.
    // This makes these objects indistinguishable from the native classes.
    // Unfortunately PublicKeyCredential does not have a javascript constructor so `extends` does not work here.
    Object.setPrototypeOf(credential.response, AuthenticatorAssertionResponse.prototype);
    Object.setPrototypeOf(credential, PublicKeyCredential.prototype);

    return credential;
  }
}
