// Relay admission tokens (docs/CLOUD_INFRA.md §2.1).
//
// A token authorizes *routing* — occupying a room — never content (every frame is Noise-E2EE
// and the relay only sees ciphertext). Two tiers keep a leaked token's blast radius small:
//
//   • membership ("m") — long-lived; "this device belongs to room R with role X". Minted by the
//     host (admin endpoint) at pairing.
//   • connection ("c") — short-lived, single-use-ish; traded from a membership token right before
//     opening a WebSocket. This is what the /relay upgrade requires.
//
// Both are stateless HMAC-signed (`payload.signature`, base64url) so the Worker verifies them
// with no KV read on the hot path; revocation is a small KV denylist checked alongside.

export type TokenType = "m" | "c";

export interface TokenClaims {
  roomId: string;
  deviceId: string;
  role: "host" | "client";
  typ: TokenType;
  iat: number;
  exp: number;
}

const enc = new TextEncoder();
const dec = new TextDecoder();

function b64url(bytes: Uint8Array): string {
  let s = "";
  for (const b of bytes) s += String.fromCharCode(b);
  return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

function b64urlDecode(s: string): Uint8Array {
  s = s.replace(/-/g, "+").replace(/_/g, "/");
  const pad = s.length % 4 ? 4 - (s.length % 4) : 0;
  const bin = atob(s + "=".repeat(pad));
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

function hmacKey(secret: string): Promise<CryptoKey> {
  return crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
    "sign",
    "verify",
  ]);
}

export async function signToken(secret: string, claims: TokenClaims): Promise<string> {
  const payload = b64url(enc.encode(JSON.stringify(claims)));
  const sig = await crypto.subtle.sign("HMAC", await hmacKey(secret), enc.encode(payload));
  return `${payload}.${b64url(new Uint8Array(sig))}`;
}

export type VerifyResult = { ok: true; claims: TokenClaims } | { ok: false; error: string };

export async function verifyToken(secret: string, token: string, now = Date.now()): Promise<VerifyResult> {
  const dot = token.indexOf(".");
  if (dot <= 0) return { ok: false, error: "malformed" };
  const payload = token.slice(0, dot);
  const sig = token.slice(dot + 1);

  let valid = false;
  try {
    valid = await crypto.subtle.verify("HMAC", await hmacKey(secret), b64urlDecode(sig), enc.encode(payload));
  } catch {
    return { ok: false, error: "badsig" };
  }
  if (!valid) return { ok: false, error: "badsig" };

  let claims: TokenClaims;
  try {
    claims = JSON.parse(dec.decode(b64urlDecode(payload)));
  } catch {
    return { ok: false, error: "badpayload" };
  }
  if (typeof claims.exp !== "number" || claims.exp * 1000 < now) return { ok: false, error: "expired" };
  return { ok: true, claims };
}

export function mintClaims(opts: {
  roomId: string;
  deviceId: string;
  role: "host" | "client";
  typ: TokenType;
  ttlSeconds: number;
  now?: number;
}): TokenClaims {
  const iat = Math.floor((opts.now ?? Date.now()) / 1000);
  return {
    roomId: opts.roomId,
    deviceId: opts.deviceId,
    role: opts.role,
    typ: opts.typ,
    iat,
    exp: iat + opts.ttlSeconds,
  };
}

/** Membership tokens live ~90 days; connection tokens ~2 minutes. */
export const MEMBERSHIP_TTL = 90 * 24 * 60 * 60;
export const CONNECTION_TTL = 120;
