import * as Base64 from '../core/Base64.js'
import * as Cbor from '../core/Cbor.js'
import type * as Errors from '../core/Errors.js'
import type * as Hex from '../core/Hex.js'
import type { Compute } from '../core/internal/types.js'
import * as PublicKey from '../core/PublicKey.js'
import {
  base64UrlOptions,
  bufferSourceToBytes,
  bytesToArrayBuffer,
  responseKeys,
} from './internal/utils.js'
import type * as Types from './Types.js'

/** A WebAuthn-flavored P256 credential. */
export type Credential<serialized extends boolean = false> = {
  attestationObject: serialized extends true ? string : ArrayBuffer
  clientDataJSON: serialized extends true ? string : ArrayBuffer
  id: string
  publicKey: serialized extends true ? Hex.Hex : PublicKey.PublicKey
  raw: Types.PublicKeyCredential<serialized>
}

/** Metadata for a WebAuthn P256 signature. */
export type SignMetadata = Compute<{
  authenticatorData: Hex.Hex
  challengeIndex?: number | undefined
  clientDataJSON: string
  typeIndex?: number | undefined
  userVerificationRequired?: boolean | undefined
}>

/**
 * Serializes a credential into a JSON-serializable
 * format.
 *
 * @example
 * ```ts twoslash
 * import { Registration, Credential } from 'ox/webauthn'
 *
 * const credential = await Registration.create({ name: 'Example' })
 *
 * const serialized = Credential.serialize(credential) // [!code focus]
 *
 * // `serialized` is JSON-serializable — send it to a server, store it, etc.
 * const json = JSON.stringify(serialized)
 * ```
 *
 * @param credential - The credential to serialize.
 * @returns The serialized credential.
 */
export function serialize(credential: Credential): Credential<true> {
  const { attestationObject, clientDataJSON, id, publicKey, raw } = credential

  const response = {} as Record<string, string>
  for (const key of responseKeys) {
    const r = raw.response as unknown as Record<string, unknown>
    let value = r[key]
    // Some properties (e.g. `authenticatorData`) are only accessible via
    // getter methods (e.g. `getAuthenticatorData()`) in certain browsers
    // and passkey providers.
    if (!(value instanceof ArrayBuffer)) {
      const getter =
        `get${key[0]!.toUpperCase()}${key.slice(1)}` as keyof typeof r
      const fn = r[getter]
      if (typeof fn === 'function') value = fn.call(r)
    }
    if (value instanceof ArrayBuffer)
      response[key] = Base64.fromBytes(new Uint8Array(value), base64UrlOptions)
  }

  // Some browsers/passkey providers (e.g. Firefox + 1Password) don't expose
  // `authenticatorData` on the response object. Fall back to extracting it
  // from the CBOR-encoded `attestationObject` which always contains `authData`.
  if (!response.authenticatorData) {
    const attestation = Cbor.decode<{ authData: Uint8Array }>(
      new Uint8Array(attestationObject),
    )
    if (attestation.authData)
      response.authenticatorData = Base64.fromBytes(
        attestation.authData,
        base64UrlOptions,
      )
  }

  return {
    attestationObject: Base64.fromBytes(
      new Uint8Array(attestationObject),
      base64UrlOptions,
    ),
    clientDataJSON: Base64.fromBytes(
      new Uint8Array(clientDataJSON),
      base64UrlOptions,
    ),
    id,
    publicKey: PublicKey.toHex(publicKey),
    raw: {
      id: raw.id,
      type: raw.type,
      authenticatorAttachment: raw.authenticatorAttachment,
      rawId: Base64.fromBytes(bufferSourceToBytes(raw.rawId), base64UrlOptions),
      response: response as unknown as Types.AuthenticatorResponse<true>,
    },
  }
}

export declare namespace serialize {
  type ErrorType =
    | Base64.fromBytes.ErrorType
    | PublicKey.toHex.ErrorType
    | Errors.GlobalErrorType
}

/**
 * Deserializes a serialized credential.
 *
 * @example
 * ```ts twoslash
 * import { Credential } from 'ox/webauthn'
 *
 * const credential = Credential.deserialize({ // [!code focus]
 *   attestationObject: 'o2NmbXRkbm9uZQ...', // [!code focus]
 *   clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0', // [!code focus]
 *   id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus]
 *   publicKey: '0x04ab891400140fc4f8e941ce0ff90e419de9470acaca613bbd717a4775435031a7d884318e919fd3b3e5a631d866d8a380b44063e70f0c381ee16e0652f7f97554', // [!code focus]
 *   raw: { // [!code focus]
 *     id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus]
 *     type: 'public-key', // [!code focus]
 *     authenticatorAttachment: 'platform', // [!code focus]
 *     rawId: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus]
 *     response: { // [!code focus]
 *       clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0', // [!code focus]
 *     }, // [!code focus]
 *   }, // [!code focus]
 * }) // [!code focus]
 * ```
 *
 * @param credential - The serialized credential.
 * @returns The deserialized credential.
 */
export function deserialize(credential: Credential<true>): Credential {
  const { attestationObject, clientDataJSON, id, publicKey, raw } = credential

  const response = Object.create(null) as Record<string, ArrayBuffer>
  for (const key of responseKeys) {
    const value = (raw.response as unknown as Record<string, string>)[key]
    if (value) response[key] = bytesToArrayBuffer(Base64.toBytes(value))
  }

  return {
    attestationObject: bytesToArrayBuffer(Base64.toBytes(attestationObject)),
    clientDataJSON: bytesToArrayBuffer(Base64.toBytes(clientDataJSON)),
    id,
    publicKey: PublicKey.from(publicKey),
    raw: {
      id: raw.id,
      type: raw.type,
      authenticatorAttachment: raw.authenticatorAttachment,
      rawId: bytesToArrayBuffer(Base64.toBytes(raw.rawId)),
      response: response as unknown as Types.AuthenticatorResponse,
      getClientExtensionResults: () => ({}),
    },
  }
}

export declare namespace deserialize {
  type ErrorType =
    | Base64.toBytes.ErrorType
    | PublicKey.from.ErrorType
    | Errors.GlobalErrorType
}
