/**
 * T2 · §4.3 capability-token ISSUE. `sub`/`host`/`account` come from `IssueArgs.principal`,
 * never a client field (INV3). Connect-scoped: short TTL 30–60 s (Finding-4 leak blast-radius),
 * single host (no wildcard), least-privilege rights, DPoP `cnf.jkt` proof-of-possession binding.
 */
import type { CapabilityRight } from 'relay-contracts'
import { CapabilityRightSchema } from 'relay-contracts'
import type { AuthenticatedPrincipal } from '../types.js'
import { signPaseto } from '../crypto/paseto.js'
import { CapabilityError } from './errors.js'

/** Connect-scoped TTL clamp (Finding-4): 30–60 s; longer values are REFUSED at issue. */
export const CONNECT_TOKEN_MIN_TTL_SEC = 30 as const
export const CONNECT_TOKEN_MAX_TTL_SEC = 60 as const

export interface IssueArgs {
  readonly principal: AuthenticatedPrincipal // sub := principal.accountId (INV3, T1 convention)
  readonly aud: string // subdomain (Host-confusion guard, INV1)
  readonly host: string // single host_id, verified owned by principal.accountId
  readonly rights: readonly CapabilityRight[] // least-privilege subset
  readonly ttlSeconds: number // clamped to [30, 60]
  readonly cnfJkt: string // base64url SHA-256 JWK thumbprint of the client's ephemeral public key
}

/** Internal token body = frozen §4.3 fields + the ADDITIVE `cnf.jkt` PoP claim (RFC 7800). */
export interface TokenBody {
  readonly sub: string
  readonly aud: string
  readonly host: string
  readonly rights: readonly CapabilityRight[]
  readonly iat: number
  readonly exp: number
  readonly jti: string
  readonly cnf: { readonly jkt: string }
}

function randomJti(): string {
  return globalThis.crypto.randomUUID()
}

export async function issueCapabilityToken(
  a: IssueArgs,
  signingKey: CryptoKey,
  now: number,
): Promise<string> {
  if (a.principal.accountId.length === 0) throw new CapabilityError('empty_sub')
  if (a.host === '*' || a.host.length === 0) throw new CapabilityError('wildcard_host')
  if (a.ttlSeconds > CONNECT_TOKEN_MAX_TTL_SEC) throw new CapabilityError('ttl_too_long')
  for (const r of a.rights) CapabilityRightSchema.parse(r)
  if (a.rights.length === 0) throw new CapabilityError('malformed', 'rights must be non-empty')
  if (a.cnfJkt.length === 0) throw new CapabilityError('no_cnf')
  // A base64url SHA-256 JWK thumbprint is exactly 43 chars of [A-Za-z0-9_-] (hardening, Finding-4).
  if (!/^[A-Za-z0-9_-]{43}$/.test(a.cnfJkt)) throw new CapabilityError('bad_cnf')

  const ttl = Math.max(a.ttlSeconds, CONNECT_TOKEN_MIN_TTL_SEC)
  const body: TokenBody = {
    sub: a.principal.accountId, // INV3: the account is the authoritative cross-tenant unit
    aud: a.aud,
    host: a.host,
    rights: [...new Set(a.rights)],
    iat: now,
    exp: now + ttl,
    jti: randomJti(),
    cnf: { jkt: a.cnfJkt },
  }
  return signPaseto(body, signingKey)
}
