/**
 * B6 (Phase-1 STAGING) — browser-side DPoP proof-of-possession for the §4.3 capability-token WS
 * upgrade.
 *
 * The operator's browser mints an EPHEMERAL Ed25519 keypair (WebCrypto), computes its RFC 7638/8037
 * JWK thumbprint `jkt`, and binds the minted capability token to it (`cnf.jkt`) at `POST /auth/mint`.
 * On connect it signs a DPoP proof over (htu, htm, jti, iat) with the SAME private key; the relay
 * recomputes the thumbprint from the proof's embedded JWK and requires it to equal the token's
 * `cnf.jkt` (relay-auth `verifyDpopProof`). The private key NEVER leaves the page and is NEVER
 * logged (INV9); it is generated per successful login and thrown away when the tab closes.
 *
 * Wire format is byte-for-byte identical to relay-auth's `buildDpopProof` / `jwkThumbprint`
 * (cross-validated against relay-auth `verifyDpopProof`): header
 * `{typ:'dpop+ed25519', jwk:{crv:'Ed25519', kty:'OKP', x}}`, payload `{htu, htm, jti, iat}`,
 * proof = `b64u(header).b64u(payload).b64u(Ed25519sig("h.p"))`; jkt = `b64u(SHA-256(canonical JWK))`
 * with members in the REQUIRED lexicographic order `crv,kty,x` and no whitespace. base64url comes
 * from relay-contracts (the shared, isomorphic helper — never hand-rolled here).
 *
 * DELIVERY (Phase-1 gap): the browser's native WebSocket API cannot set request headers, but the
 * relay reads the DPoP proof from the `dpop` request header (relay-run browser-server.ts). Mirroring
 * the §4.3 token (which rides `Sec-WebSocket-Protocol` for exactly this reason), the proof is offered
 * as an ADDITIONAL subprotocol entry (`term.dpop.<b64url(proofJws)>`) via {@link encodeDpopSubprotocol}.
 * The current relay ignores unknown subprotocol entries (its `handleProtocols` echoes only
 * `APP_SUBPROTOCOL`), so this is handshake-safe today; a real browser connect is nonetheless denied
 * at DPoP until the relay is taught to read the proof from this entry (a relay/server-lane change,
 * validated on the VPS). See the returned notes in the B6 log entry.
 */
import { encodeBase64UrlBytes, encodeBase64UrlString, decodeBase64UrlString } from 'relay-contracts'
import { RelayWebError } from './errors'

/** DPoP HTTP method bound into every proof — a WS upgrade is a GET (mirrors relay-run `DPOP_HTM`). */
export const DPOP_HTM = 'GET' as const

/** Subprotocol entry prefix carrying the DPoP proof on the upgrade (browsers can't set headers). */
export const DPOP_SUBPROTOCOL_PREFIX = 'term.dpop.' as const

/**
 * Canonical DPoP `htu` for an audience — mirrors relay-run `htuFor` (the SINGLE definition both
 * sides use so issuance and verification never drift). `aud` is the tenant subdomain the token is
 * bound to; the relay re-derives the identical string from its own resolved authority.
 */
export function htuFor(aud: string): string {
  return `https://${aud}/ws`
}

/** A base64url SHA-256 JWK thumbprint is exactly 43 chars — matches the server's `JKT_RE`. */
const JKT_LENGTH = 43

export interface DpopKey {
  /** RFC 7638 JWK thumbprint (base64url SHA-256, 43 chars) — the `cnf.jkt` binding sent to /auth/mint. */
  readonly jkt: string
  /** Sign a FRESH DPoP proof JWS for one upgrade (unique jti; `iat = nowSec`). Never logs the key. */
  proof(htu: string, htm: string, nowSec: number): Promise<string>
}

/** DI seams so tests run deterministically (inject Node webcrypto under jsdom + a fixed jti). */
export interface DpopDeps {
  /** SubtleCrypto to use; defaults to the page's `globalThis.crypto.subtle`. */
  readonly subtle?: SubtleCrypto
  /** DPoP `jti` generator; defaults to `crypto.randomUUID()`. */
  readonly randomJti?: () => string
}

const ED25519 = { name: 'Ed25519' } as const

function defaultJti(): string {
  const c = globalThis.crypto
  if (!c || typeof c.randomUUID !== 'function') {
    throw new RelayWebError('crypto.randomUUID is unavailable — cannot mint a DPoP jti')
  }
  return c.randomUUID()
}

/** Canonical Ed25519 public JWK JSON (member order crv, kty, x) → base64url(SHA-256) thumbprint. */
async function computeJkt(subtle: SubtleCrypto, x: string): Promise<string> {
  const json = JSON.stringify({ crv: 'Ed25519', kty: 'OKP', x })
  const digest = new Uint8Array(await subtle.digest('SHA-256', new TextEncoder().encode(json)))
  return encodeBase64UrlBytes(digest)
}

/**
 * Generate an ephemeral browser DPoP key and expose its `jkt` + a per-upgrade proof signer.
 * Fails fast (typed {@link RelayWebError}) when WebCrypto/Ed25519 is unavailable.
 */
export async function createDpopKey(deps: DpopDeps = {}): Promise<DpopKey> {
  const subtle = deps.subtle ?? globalThis.crypto?.subtle
  if (!subtle) {
    throw new RelayWebError('WebCrypto SubtleCrypto is unavailable — DPoP requires Ed25519')
  }
  const randomJti = deps.randomJti ?? defaultJti

  let pair: CryptoKeyPair
  let rawPub: Uint8Array
  try {
    pair = (await subtle.generateKey(ED25519, true, ['sign', 'verify'])) as CryptoKeyPair
    rawPub = new Uint8Array(await subtle.exportKey('raw', pair.publicKey))
  } catch {
    // Never surface key material in the error (INV9).
    throw new RelayWebError('failed to generate an Ed25519 DPoP key (unsupported by this browser?)')
  }

  const x = encodeBase64UrlBytes(rawPub)
  const jkt = await computeJkt(subtle, x)
  if (jkt.length !== JKT_LENGTH) {
    throw new RelayWebError(`unexpected DPoP thumbprint length ${jkt.length} (expected ${JKT_LENGTH})`)
  }

  const proof = async (htu: string, htm: string, nowSec: number): Promise<string> => {
    const header = { typ: 'dpop+ed25519', jwk: { crv: 'Ed25519', kty: 'OKP', x } }
    const payload = { htu, htm, jti: randomJti(), iat: nowSec }
    const h = encodeBase64UrlString(JSON.stringify(header))
    const p = encodeBase64UrlString(JSON.stringify(payload))
    const sig = new Uint8Array(
      await subtle.sign(ED25519, pair.privateKey, new TextEncoder().encode(`${h}.${p}`)),
    )
    return `${h}.${p}.${encodeBase64UrlBytes(sig)}`
  }

  return { jkt, proof }
}

/** Build the DPoP subprotocol entry: `term.dpop.` + base64url(proofJws). */
export function encodeDpopSubprotocol(proofJws: string): string {
  return DPOP_SUBPROTOCOL_PREFIX + encodeBase64UrlString(proofJws)
}

/**
 * Extract the DPoP proof from a subprotocol list (the future relay-side inverse of
 * {@link encodeDpopSubprotocol}). Returns null when absent. Exposed for round-trip tests and to
 * document the exact wire contract the relay must read once it consumes the proof from the handshake.
 */
export function extractDpopFromSubprotocols(values: readonly string[]): string | null {
  const entry = values.find((v) => v.startsWith(DPOP_SUBPROTOCOL_PREFIX))
  if (entry === undefined) return null
  return decodeBase64UrlString(entry.slice(DPOP_SUBPROTOCOL_PREFIX.length))
}
