/**
 * T7 (v0.10) — thin `navigator.credentials` wrapper: base64url ↔ ArrayBuffer plumbing + error
 * mapping, kept small and pure-ish. No credential material is ever logged (INV9).
 *
 * The server (P5) is authoritative for `rpId` — it lives in the challenge options this module
 * passes straight through to the authenticator; the client NEVER derives `rpId` from
 * `location.hostname` (see §8 Open Question #5). This module only encodes/decodes and invokes the
 * ceremony; the account is established server-side from the resulting assertion (INV3).
 */
import { decodeBase64UrlBytes, encodeBase64UrlBytes } from 'relay-contracts'

/** WebAuthn ceremony error, kind-tagged so callers can map cancellation → 'rejected' cleanly. */
export class WebAuthnError extends Error {
  readonly kind: 'cancelled' | 'unsupported' | 'malformed' | 'failed'
  constructor(kind: WebAuthnError['kind'], message: string) {
    super(message)
    this.name = 'WebAuthnError'
    this.kind = kind
  }
}

export interface RegistrationResult {
  readonly id: string
  readonly rawId: string // base64url
  readonly type: string
  readonly response: {
    readonly clientDataJSON: string // base64url
    readonly attestationObject: string // base64url
  }
}

export interface AuthenticationResult {
  readonly id: string
  readonly rawId: string // base64url
  readonly type: string
  readonly response: {
    readonly clientDataJSON: string // base64url
    readonly authenticatorData: string // base64url
    readonly signature: string // base64url
    readonly userHandle: string | null // base64url or null
  }
}

export interface WebAuthnClient {
  register(challenge: PublicKeyCredentialCreationOptions): Promise<RegistrationResult>
  authenticate(challenge: PublicKeyCredentialRequestOptions): Promise<AuthenticationResult>
}

/** ArrayBuffer → base64url (lossless). */
export function bufferToBase64Url(buf: ArrayBuffer): string {
  return encodeBase64UrlBytes(new Uint8Array(buf))
}

/** base64url → ArrayBuffer (lossless inverse of {@link bufferToBase64Url}). */
export function base64UrlToBuffer(value: string): ArrayBuffer {
  const bytes = decodeBase64UrlBytes(value)
  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
}

/** Map a thrown ceremony error to a typed WebAuthnError (user cancel → 'cancelled'). */
function mapCeremonyError(err: unknown): WebAuthnError {
  if (err instanceof WebAuthnError) return err
  const name = err instanceof Error ? err.name : ''
  if (name === 'NotAllowedError' || name === 'AbortError') {
    return new WebAuthnError('cancelled', 'the passkey prompt was dismissed')
  }
  return new WebAuthnError('failed', err instanceof Error ? err.message : 'webauthn ceremony failed')
}

export function createWebAuthnClient(creds?: CredentialsContainer): WebAuthnClient {
  const container = creds ?? (typeof navigator !== 'undefined' ? navigator.credentials : undefined)
  if (!container) {
    // Fail fast at construction so the UI can offer a fallback (never a silent no-op).
    throw new WebAuthnError('unsupported', 'WebAuthn is not available in this environment')
  }

  return {
    async register(challenge: PublicKeyCredentialCreationOptions): Promise<RegistrationResult> {
      let cred: Credential | null
      try {
        cred = await container.create({ publicKey: challenge })
      } catch (err) {
        throw mapCeremonyError(err)
      }
      if (!cred) throw new WebAuthnError('failed', 'no credential returned')
      const pk = cred as PublicKeyCredential
      const resp = pk.response as AuthenticatorAttestationResponse
      return {
        id: pk.id,
        rawId: bufferToBase64Url(pk.rawId),
        type: pk.type,
        response: {
          clientDataJSON: bufferToBase64Url(resp.clientDataJSON),
          attestationObject: bufferToBase64Url(resp.attestationObject),
        },
      }
    },

    async authenticate(
      challenge: PublicKeyCredentialRequestOptions,
    ): Promise<AuthenticationResult> {
      let cred: Credential | null
      try {
        cred = await container.get({ publicKey: challenge })
      } catch (err) {
        throw mapCeremonyError(err)
      }
      if (!cred) throw new WebAuthnError('failed', 'no assertion returned')
      const pk = cred as PublicKeyCredential
      const resp = pk.response as AuthenticatorAssertionResponse
      return {
        id: pk.id,
        rawId: bufferToBase64Url(pk.rawId),
        type: pk.type,
        response: {
          clientDataJSON: bufferToBase64Url(resp.clientDataJSON),
          authenticatorData: bufferToBase64Url(resp.authenticatorData),
          signature: bufferToBase64Url(resp.signature),
          userHandle: resp.userHandle ? bufferToBase64Url(resp.userHandle) : null,
        },
      }
    },
  }
}
