// Relay presence → pool wake (docs/COVALENCE_RUNNER.md §3, §0.2 item 3e).
//
// The mapping problem: the relay knows rooms (roomId = sha256 of the RUNNER's static key,
// minted inside the container) while this control plane knows pools (poolId = sha256 of the
// PROVISIONING DEVICE's static key) — two different identities on purpose. The bridge is
// self-registering: every runner heartbeat carries that runner's roomId, and the Worker refreshes
// `room:<roomId>` → `{poolId, instanceId}` in KV. Each runner is its own mesh peer with its own
// room, so the mapping records *which* runner slot owns the room — waking reboots exactly that
// slept runner. The record outlives the container (that's the point — wake exists to boot a slept
// container), and a runner identity rotation simply registers the new room on its first heartbeat.
//
// The hook itself (`POST /v1/hook/wake`, body `{roomId}`) is called by the relay's Room DO when
// a client connects to a room with no host present. It is authenticated by a dedicated shared
// secret (RUNNER_WAKE_SECRET — deliberately NOT the admin bearer: the relay holds it, and it
// grants exactly one power, waking). Unset ⇒ the hook is disabled and answers 501.

export interface KVLike {
  get(key: string): Promise<string | null>;
  put(key: string, value: string, opts?: { expirationTtl?: number }): Promise<void>;
}

/** room→pool records refresh on every heartbeat; the long TTL only reaps pools that stopped
 * heartbeating entirely (deleted / abandoned), matching the pool-credential lifecycle. */
export const ROOM_KEY_TTL = 90 * 24 * 60 * 60;

const ROOM_ID_PATTERN = /^[0-9a-f]{64}$/;

/** The room→pool mapping value. Stored as JSON so wake can reboot the specific runner slot; a
 * legacy record was a bare `poolId` string (older Worker, host-only), tolerated on read. */
export interface RoomMapping {
  poolId: string;
  instanceId: string;
}

/** Parse a stored room record — JSON `{poolId, instanceId}` now, or a legacy bare-poolId string
 * (host-only era) which we resolve to the `runner-1`-equivalent by returning no instanceId so the
 * hook falls back to a bare provision. */
export function parseRoomMapping(raw: string | null): RoomMapping | { poolId: string; instanceId?: undefined } | null {
  if (!raw) return null;
  try {
    const parsed = JSON.parse(raw) as Record<string, unknown>;
    if (typeof parsed?.poolId === "string" && typeof parsed?.instanceId === "string") {
      return { poolId: parsed.poolId, instanceId: parsed.instanceId };
    }
  } catch {
    // Not JSON — a legacy bare poolId string.
  }
  return { poolId: raw };
}

/** Register (refresh) the room→pool mapping a runner heartbeat carries. Every runner slot claims
 * its own room (each is a full mesh peer); a malformed roomId is dropped rather than stored (the
 * value is attacker-adjacent: it rides an instance-token request). */
export async function registerRoomMapping(
  kv: KVLike,
  poolId: string,
  instanceId: string,
  body: Record<string, unknown> | null,
): Promise<void> {
  const roomId = body?.roomId;
  if (typeof roomId !== "string" || !ROOM_ID_PATTERN.test(roomId)) return;
  await kv.put(`room:${roomId}`, JSON.stringify({ poolId, instanceId }), { expirationTtl: ROOM_KEY_TTL });
}

/** The wake hook: authenticate the relay, resolve roomId → `{poolId, instanceId}`, and forward a
 * pool-auth `/wake {instanceId}` (reboot that specific runner slot) to the pool's DO. Pure over
 * injected deps — node:test drives it with fakes. */
export async function handleWakeHook(opts: {
  kv: KVLike;
  secret: string | undefined;
  token: string | null;
  body: Record<string, unknown> | null;
  forward: (poolId: string, instanceId: string | undefined) => Promise<Response>;
}): Promise<Response> {
  if (!opts.secret) return new Response("wake hook not configured", { status: 501 });
  if (!opts.token || opts.token !== opts.secret) return new Response("unauthorized", { status: 401 });

  const roomId = opts.body?.roomId;
  if (typeof roomId !== "string" || !ROOM_ID_PATTERN.test(roomId)) {
    return new Response("roomId required", { status: 422 });
  }
  const mapping = parseRoomMapping(await opts.kv.get(`room:${roomId}`));
  if (!mapping) return new Response("unknown room", { status: 404 });
  if (await opts.kv.get(`revoked:${mapping.poolId}`)) {
    return new Response("unauthorized: revoked", { status: 401 });
  }
  return opts.forward(mapping.poolId, mapping.instanceId);
}
