import { p256 } from '@noble/curves/p256'
import type * as Errors from '../Errors.js'
import * as Hex from '../Hex.js'
import * as PublicKey from '../PublicKey.js'
import { CredentialCreationFailedError } from '../WebAuthnP256.js'

/** @internal */
export type AttestationConveyancePreference =
  | 'direct'
  | 'enterprise'
  | 'indirect'
  | 'none'

/** @internal */
export type AuthenticatorAttachment = 'cross-platform' | 'platform'

/** @internal */
export type AuthenticatorTransport =
  | 'ble'
  | 'hybrid'
  | 'internal'
  | 'nfc'
  | 'usb'

/** @internal */
export type COSEAlgorithmIdentifier = number

/** @internal */
export type CredentialMediationRequirement =
  | 'conditional'
  | 'optional'
  | 'required'
  | 'silent'

/** @internal */
export type PublicKeyCredentialType = 'public-key'

/** @internal */
export type ResidentKeyRequirement = 'discouraged' | 'preferred' | 'required'

/** @internal */
export type UserVerificationRequirement =
  | 'discouraged'
  | 'preferred'
  | 'required'

/** @internal */
export type LargeBlobSupport = {
  support: 'required' | 'preferred'
}

/** @internal */
export type BufferSource = ArrayBufferView | ArrayBuffer

/** @internal */
export type PrfExtension = Record<'eval', Record<'first', Uint8Array>>

/** @internal */
export interface AuthenticationExtensionsClientInputs {
  appid?: string
  credProps?: boolean
  hmacCreateSecret?: boolean
  minPinLength?: boolean
  prf?: PrfExtension
  largeBlob?: LargeBlobSupport
}

/** @internal */
export interface AuthenticatorSelectionCriteria {
  authenticatorAttachment?: AuthenticatorAttachment
  requireResidentKey?: boolean
  residentKey?: ResidentKeyRequirement
  userVerification?: UserVerificationRequirement
}

/** @internal */
export interface Credential {
  readonly id: string
  readonly type: string
}

/** @internal */
export interface CredentialCreationOptions {
  publicKey?: PublicKeyCredentialCreationOptions
  signal?: AbortSignal
}

/** @internal */
export interface CredentialRequestOptions {
  mediation?: CredentialMediationRequirement
  publicKey?: PublicKeyCredentialRequestOptions
  signal?: AbortSignal
}

/** @internal */
export interface PublicKeyCredential extends Credential {
  readonly authenticatorAttachment: string | null
  readonly rawId: ArrayBuffer
  readonly response: AuthenticatorResponse
  getClientExtensionResults(): AuthenticationExtensionsClientOutputs
}

/** @internal */
export interface PublicKeyCredentialCreationOptions {
  attestation?: AttestationConveyancePreference
  authenticatorSelection?: AuthenticatorSelectionCriteria
  challenge: BufferSource
  excludeCredentials?: PublicKeyCredentialDescriptor[]
  extensions?: AuthenticationExtensionsClientInputs
  pubKeyCredParams: PublicKeyCredentialParameters[]
  rp: PublicKeyCredentialRpEntity
  timeout?: number
  user: PublicKeyCredentialUserEntity
}

/** @internal */
export interface PublicKeyCredentialDescriptor {
  id: BufferSource
  transports?: AuthenticatorTransport[]
  type: PublicKeyCredentialType
}

/** @internal */
export interface PublicKeyCredentialEntity {
  name: string
}

/** @internal */
export interface PublicKeyCredentialParameters {
  alg: COSEAlgorithmIdentifier
  type: PublicKeyCredentialType
}

/** @internal */
export interface PublicKeyCredentialRequestOptions {
  allowCredentials?: PublicKeyCredentialDescriptor[]
  challenge: BufferSource
  extensions?: AuthenticationExtensionsClientInputs
  rpId?: string
  timeout?: number
  userVerification?: UserVerificationRequirement
}

/** @internal */
export interface PublicKeyCredentialRpEntity extends PublicKeyCredentialEntity {
  id?: string
}

/** @internal */
export interface PublicKeyCredentialUserEntity
  extends PublicKeyCredentialEntity {
  displayName: string
  id: BufferSource
}

/**
 * Parses an ASN.1 signature into a r and s value.
 *
 * @internal
 */
export function parseAsn1Signature(bytes: Uint8Array) {
  const r_start = bytes[4] === 0 ? 5 : 4
  const r_end = r_start + 32
  const s_start = bytes[r_end + 2] === 0 ? r_end + 3 : r_end + 2

  const r = BigInt(Hex.fromBytes(bytes.slice(r_start, r_end)))
  const s = BigInt(Hex.fromBytes(bytes.slice(s_start)))

  return {
    r,
    s: s > p256.CURVE.n / 2n ? p256.CURVE.n - s : s,
  }
}

/**
 * Parses a public key into x and y coordinates from the public key
 * defined on the credential.
 *
 * @internal
 */
export async function parseCredentialPublicKey(
  response: AuthenticatorAttestationResponse,
): Promise<PublicKey.PublicKey> {
  try {
    const publicKeyBuffer = response.getPublicKey()
    if (!publicKeyBuffer) throw new CredentialCreationFailedError()

    // Converting `publicKeyBuffer` throws when credential is created by 1Password Firefox Add-on
    const publicKeyBytes = new Uint8Array(publicKeyBuffer)
    const cryptoKey = await crypto.subtle.importKey(
      'spki',
      new Uint8Array(publicKeyBytes),
      {
        name: 'ECDSA',
        namedCurve: 'P-256',
        hash: 'SHA-256',
      },
      true,
      ['verify'],
    )
    const publicKey = new Uint8Array(
      await crypto.subtle.exportKey('raw', cryptoKey),
    )
    return PublicKey.from(publicKey)
  } catch (error) {
    // Fallback for 1Password Firefox Add-on restricts access to certain credential properties
    // so we need to use `attestationObject` to extract the public key.
    // https://github.com/passwordless-id/webauthn/issues/50#issuecomment-2072902094
    if ((error as Error).message !== 'Permission denied to access object')
      throw error

    const data = new Uint8Array(response.attestationObject)
    const coordinateLength = 0x20
    const cborPrefix = 0x58

    const findStart = (key: number) => {
      const coordinate = new Uint8Array([key, cborPrefix, coordinateLength])
      for (let i = 0; i < data.length - coordinate.length; i++)
        if (coordinate.every((byte, j) => data[i + j] === byte))
          return i + coordinate.length
      throw new CredentialCreationFailedError()
    }

    const xStart = findStart(0x21)
    const yStart = findStart(0x22)

    return PublicKey.from(
      new Uint8Array([
        0x04,
        ...data.slice(xStart, xStart + coordinateLength),
        ...data.slice(yStart, yStart + coordinateLength),
      ]),
    )
  }
}

export declare namespace parseCredentialPublicKey {
  type ErrorType = CredentialCreationFailedError | Errors.GlobalErrorType
}
