/**
 * Minimal PASETO v4.public token (Ed25519) — the format the plan explicitly RECOMMENDS for the
 * §4.3 capability token (open-Q #1: "recommend PASETO v4.public: no alg-confusion, no `alg:none`
 * foot-gun"). Self-contained: no external dependency, no native binary. The header is fixed
 * (`v4.public.`) so there is NO `alg` field to confuse — the algorithm is Ed25519 by construction.
 *
 * INTEGRATION NOTE: the concrete token-format pick (PASETO v4.public vs JWS EdDSA) awaits a
 * one-line PLAN_RELAY_INDEX §4.3 confirmation (plan §5 open-Q #1). This implements the plan's
 * stated recommendation; swapping to JWS EdDSA would touch only this file + verify.ts.
 */
import { encodeBase64UrlBytes, decodeBase64UrlBytes } from 'relay-contracts'
import { signEd25519, verifyEd25519 } from './ed25519.js'

const HEADER = 'v4.public.'
const SIG_BYTES = 64
const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder('utf-8', { fatal: true })

/** Little-endian uint64 (PASETO PAE length prefix; MSB of the high word is cleared per spec). */
function le64(n: number): Uint8Array {
  const out = new Uint8Array(8)
  let value = n
  for (let i = 0; i < 8; i++) {
    if (i === 7) value &= 0x7f
    out[i] = value & 0xff
    value = Math.floor(value / 256)
  }
  return out
}

/** PASETO Pre-Authentication Encoding (PAE) of a list of byte pieces. */
function pae(pieces: readonly Uint8Array[]): Uint8Array {
  const parts: Uint8Array[] = [le64(pieces.length)]
  for (const piece of pieces) {
    parts.push(le64(piece.length))
    parts.push(piece)
  }
  let total = 0
  for (const p of parts) total += p.length
  const out = new Uint8Array(total)
  let off = 0
  for (const p of parts) {
    out.set(p, off)
    off += p.length
  }
  return out
}

function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
  const out = new Uint8Array(a.length + b.length)
  out.set(a, 0)
  out.set(b, a.length)
  return out
}

/** Sign an arbitrary JSON-serializable claims object into a `v4.public.` token string. */
export async function signPaseto(claims: unknown, privateKey: CryptoKey): Promise<string> {
  const message = textEncoder.encode(JSON.stringify(claims))
  const header = textEncoder.encode(HEADER)
  const m2 = pae([header, message, new Uint8Array(0), new Uint8Array(0)])
  const sig = await signEd25519(privateKey, m2)
  return HEADER + encodeBase64UrlBytes(concat(message, sig))
}

export class PasetoError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'PasetoError'
  }
}

/** Verify a `v4.public.` token's Ed25519 signature and return the decoded JSON claims. */
export async function verifyPaseto(token: string, publicKey: CryptoKey): Promise<unknown> {
  if (!token.startsWith(HEADER)) throw new PasetoError('bad PASETO header')
  const body = decodeBase64UrlBytes(token.slice(HEADER.length))
  if (body.length < SIG_BYTES) throw new PasetoError('truncated PASETO body')
  const message = body.subarray(0, body.length - SIG_BYTES)
  const sig = body.subarray(body.length - SIG_BYTES)
  const header = textEncoder.encode(HEADER)
  const m2 = pae([header, message, new Uint8Array(0), new Uint8Array(0)])
  const ok = await verifyEd25519(publicKey, sig, m2)
  if (!ok) throw new PasetoError('signature verification failed')
  try {
    return JSON.parse(textDecoder.decode(message))
  } catch {
    throw new PasetoError('claims are not valid JSON')
  }
}

/** Decode claims WITHOUT verifying the signature (only safe AFTER a prior verify — e.g. read exp). */
export function peekPasetoClaims(token: string): unknown {
  if (!token.startsWith(HEADER)) throw new PasetoError('bad PASETO header')
  const body = decodeBase64UrlBytes(token.slice(HEADER.length))
  if (body.length < SIG_BYTES) throw new PasetoError('truncated PASETO body')
  const message = body.subarray(0, body.length - SIG_BYTES)
  try {
    return JSON.parse(textDecoder.decode(message))
  } catch {
    throw new PasetoError('claims are not valid JSON')
  }
}
