/**
 * T2 · §4.4 `ClientHello.deviceAuthProof` — the SOLE issuer/verifier (Finding-7 / INDEX §6b).
 *
 * P5 mints from the authenticated `AuthenticatedPrincipal` (asserts `principal.accountId`), bound to
 * `{ clientEphPub, clientNonce }` — a per-handshake, non-replayable binding (NOT a static bearer,
 * NOT transcriptHash). P4 consumes `verifyDeviceProof` as an injected dep and never re-derives the
 * account binding. A captured proof replayed into a different freshly-keyed client_hello → false.
 */
import { encodeBase64UrlBytes, decodeBase64UrlBytes } from 'relay-contracts'
import type { AuthenticatedPrincipal } from '../types.js'
import { signEd25519, verifyEd25519 } from '../crypto/ed25519.js'
import { getVerifyKey } from '../config/keys.js'

export type DeviceProofBinding = {
  readonly clientEphPub: Uint8Array
  readonly clientNonce: Uint8Array
}

const DOMAIN = 'relay-auth/device-auth/v1'
const PROOF_MAX_AGE_SEC = 120

interface DeviceProofPayload {
  readonly acct: string
  readonly iat: number
}

function concatAll(pieces: readonly Uint8Array[]): Uint8Array {
  let total = 0
  for (const p of pieces) total += p.length
  const out = new Uint8Array(total)
  let off = 0
  for (const p of pieces) {
    out.set(p, off)
    off += p.length
  }
  return out
}

function le32(n: number): Uint8Array {
  const out = new Uint8Array(4)
  out[0] = n & 0xff
  out[1] = (n >>> 8) & 0xff
  out[2] = (n >>> 16) & 0xff
  out[3] = (n >>> 24) & 0xff
  return out
}

/** Domain-separated signed message = DOMAIN ‖ acct ‖ len(ceph)‖ceph ‖ len(cnon)‖cnon ‖ iat. */
function proofMessage(acct: string, binding: DeviceProofBinding, iat: number): Uint8Array {
  const enc = new TextEncoder()
  return concatAll([
    enc.encode(DOMAIN),
    enc.encode(acct),
    le32(binding.clientEphPub.length),
    binding.clientEphPub,
    le32(binding.clientNonce.length),
    binding.clientNonce,
    le32(iat),
  ])
}

/** Client-side minting — backs the §4.4 `DeviceAuthProofProvider.proofFor` P4 injects. */
export async function signDeviceAuthProof(
  principal: AuthenticatedPrincipal,
  binding: DeviceProofBinding,
  signingKey: CryptoKey,
  now: number,
): Promise<string> {
  const payload: DeviceProofPayload = { acct: principal.accountId, iat: now }
  const message = proofMessage(payload.acct, binding, payload.iat)
  const sig = await signEd25519(signingKey, message)
  const p = encodeBase64UrlBytes(new TextEncoder().encode(JSON.stringify(payload)))
  return `${p}.${encodeBase64UrlBytes(sig)}`
}

/** Host-side verification — the injected `verifyDeviceProof` P4's createHostHandshake calls. */
export async function verifyDeviceProof(
  proof: string,
  binding: DeviceProofBinding,
  now: number,
): Promise<boolean> {
  const parts = proof.split('.')
  if (parts.length !== 2) return false
  const [p, s] = parts as [string, string]
  let payload: DeviceProofPayload
  let sig: Uint8Array
  try {
    payload = JSON.parse(new TextDecoder().decode(decodeBase64UrlBytes(p))) as DeviceProofPayload
    sig = decodeBase64UrlBytes(s)
  } catch {
    return false
  }
  if (typeof payload.acct !== 'string' || payload.acct.length === 0) return false
  if (typeof payload.iat !== 'number' || Math.abs(now - payload.iat) > PROOF_MAX_AGE_SEC) return false
  const message = proofMessage(payload.acct, binding, payload.iat)
  return verifyEd25519(getVerifyKey(), sig, message)
}

/** Read the account a proof asserts (only meaningful after `verifyDeviceProof` returns true). */
export function deviceProofAccount(proof: string): string | null {
  const parts = proof.split('.')
  if (parts.length !== 2) return null
  try {
    const payload = JSON.parse(
      new TextDecoder().decode(decodeBase64UrlBytes(parts[0]!)),
    ) as DeviceProofPayload
    return typeof payload.acct === 'string' ? payload.acct : null
  } catch {
    return null
  }
}
