// Runner instance tokens (docs/COVALENCE_RUNNER.md §8).
//
// A token authorizes *pool plumbing* — a runner's heartbeats — never content
// (devices talk to the runner over the Noise-E2EE relay path; this control plane only ever sees
// lifecycle metadata). Same stateless HMAC `payload.signature` format as the relay's tokens
// (cloud/nucleic-edge/src/relayToken.ts), duplicated here rather than imported so the two
// Workers stay independently deployable; the format is pinned by tests on both sides.
//
//   • instance ("i") — held by a running container; scoped to one (poolId, instanceId).
//   • pool credential — NOT a signed token: `poolId.poolSecret`, hash-only in KV (see enroll.ts),
//     presented by user devices (Settings) exactly like the relay's host credential.

export type TokenType = "i";

export interface TokenClaims {
  poolId: string;
  instanceId: string;
  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 (claims.typ !== "i") return { ok: false, error: "badtype" };
  if (typeof claims.exp !== "number" || claims.exp * 1000 < now) return { ok: false, error: "expired" };
  return { ok: true, claims };
}

export function mintClaims(opts: {
  poolId: string;
  instanceId: string;
  ttlSeconds: number;
  now?: number;
}): TokenClaims {
  const iat = Math.floor((opts.now ?? Date.now()) / 1000);
  return {
    poolId: opts.poolId,
    instanceId: opts.instanceId,
    typ: "i",
    iat,
    exp: iat + opts.ttlSeconds,
  };
}

/** Instance tokens live ~30 days; the pool re-mints on every accepted heartbeat, so a live
 * container's token never lapses and a stopped one's ages out. */
export const INSTANCE_TTL = 30 * 24 * 60 * 60;
