// Nucleic edge Worker.
//
//   Phase A — DAI heartbeat (daily active installs) → Workers Analytics Engine.
//   Phase B — WebSocket relay (Durable Object `Room`) + relay-token admission + relay DAU.
//   Phase C — APNS approval push (relay-triggered, content-free).
//
// See docs/CLOUD_INFRA.md and cloud/nucleic-edge/README.md.

import type { Env } from "./env";
import { parseHeartbeat, type Heartbeat } from "./heartbeat";
import { apnsConfig, approvalPayload, sendApns } from "./apns";
import {
  CONNECTION_TTL,
  MEMBERSHIP_TTL,
  mintClaims,
  signToken,
  type TokenClaims,
  verifyToken,
} from "./relayToken";

export { Room } from "./room";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const { pathname } = url;

    if (request.method === "GET" && pathname === "/health") return new Response("ok\n");

    if (pathname === "/v1/heartbeat") return method(request, "POST", () => handleHeartbeat(request, env));

    // --- relay ---
    if (pathname === "/relay") return handleRelay(request, env, url);
    if (pathname === "/v1/relay/token") return method(request, "POST", () => mintMembership(request, env));
    if (pathname === "/v1/relay/connect") return method(request, "POST", () => mintConnection(request, env));
    if (pathname === "/v1/relay/revoke") return method(request, "POST", () => revoke(request, env));

    // --- push (out-of-band wake; the in-band path is the Room "wake" control frame) ---
    if (pathname === "/v1/push/notify") return method(request, "POST", () => pushNotify(request, env));

    return new Response("not found", { status: 404 });
  },
} satisfies ExportedHandler<Env>;

// MARK: - DAI — daily active installs (Phase A)

async function handleHeartbeat(request: Request, env: Env): Promise<Response> {
  let body: unknown;
  try {
    body = await request.json();
  } catch {
    return new Response("bad json", { status: 400 });
  }
  const parsed = parseHeartbeat(body);
  if (!parsed.ok) return new Response(`bad field: ${parsed.error}`, { status: 422 });
  // No IP read or stored — the heartbeat is anonymous.
  writeHeartbeat(env, parsed.value);
  return new Response(null, { status: 204 });
}

// Indexed by the anonymous install UUID → `count(DISTINCT index1)` is a daily-active-*install*
// count (one user across two devices counts twice). For a per-user count, see writeRelayDau.
function writeHeartbeat(env: Env, hb: Heartbeat): void {
  env.NUCLEIC_DAI.writeDataPoint({
    indexes: [hb.installId],
    blobs: [hb.platform, hb.osVersion, hb.channel, hb.appVersion, hb.build, hb.arch, hb.locale],
    doubles: [1],
  });
}

// MARK: - Relay (Phase B)

async function handleRelay(request: Request, env: Env, url: URL): Promise<Response> {
  if (request.headers.get("upgrade") !== "websocket") {
    return new Response("expected websocket", { status: 426 });
  }
  if (!env.RELAY_TOKEN_SECRET) return new Response("relay not configured", { status: 503 });

  const token = url.searchParams.get("t") ?? bearer(request);
  if (!token) return new Response("missing token", { status: 401 });

  const v = await verifyToken(env.RELAY_TOKEN_SECRET, token);
  if (!v.ok) return new Response(`token ${v.error}`, { status: 401 });
  if (v.claims.typ !== "c") return new Response("connection token required", { status: 401 });
  if (await env.NUCLEIC_RELAY_TOKENS.get(`revoked:${v.claims.deviceId}`)) {
    return new Response("revoked", { status: 403 });
  }

  // Admission succeeded — count this connection toward the relay DAU (indexed by roomId ≈ user).
  writeRelayDau(env, v.claims);

  // Route to the room's DO, carrying the verified identity (the DO trusts these headers because
  // only this Worker can reach it).
  const stub = env.ROOMS.get(env.ROOMS.idFromName(v.claims.roomId));
  const headers = new Headers(request.headers);
  headers.set("x-nucleic-role", v.claims.role);
  headers.set("x-nucleic-device", v.claims.deviceId);
  return stub.fetch(new Request(url.toString(), { method: request.method, headers, body: request.body }));
}

// Relay DAU — a true daily-active-*user* metric. Unlike the anonymous DAI heartbeat (indexed by a
// random install UUID), the relay carries a real identity: `roomId` is derived from the host's
// static public key (docs/CLOUD_INFRA.md §2.1), so one room ≈ one user/account. Indexing on it
// makes `count(DISTINCT index1)` count users, not installs. `role`/`deviceId` ride along as blobs
// for slicing (e.g. host vs client, distinct devices per user). Emitted on every admitted
// connection; same-day reconnects collapse under the DISTINCT.
function writeRelayDau(env: Env, claims: TokenClaims): void {
  env.NUCLEIC_RELAY_DAU.writeDataPoint({
    indexes: [claims.roomId],
    blobs: [claims.role, claims.deviceId],
    doubles: [1],
  });
}

/** Host mints a long-lived membership token for a paired device (admin-authenticated). */
async function mintMembership(request: Request, env: Env): Promise<Response> {
  if (!env.RELAY_TOKEN_SECRET) return new Response("relay not configured", { status: 503 });
  if (!adminOk(request, env)) return unauthorized();

  const body = (await safeJson(request)) as { roomId?: string; deviceId?: string; role?: string } | null;
  if (!body?.roomId || !body?.deviceId) return new Response("roomId + deviceId required", { status: 422 });

  const claims = mintClaims({
    roomId: body.roomId,
    deviceId: body.deviceId,
    role: body.role === "host" ? "host" : "client",
    typ: "m",
    ttlSeconds: MEMBERSHIP_TTL,
  });
  return json({ token: await signToken(env.RELAY_TOKEN_SECRET, claims), exp: claims.exp });
}

/** Trade a membership token for a short-lived connection token (presented on the WS upgrade). */
async function mintConnection(request: Request, env: Env): Promise<Response> {
  if (!env.RELAY_TOKEN_SECRET) return new Response("relay not configured", { status: 503 });

  const m = bearer(request);
  const v = m ? await verifyToken(env.RELAY_TOKEN_SECRET, m) : null;
  if (!v || !v.ok || v.claims.typ !== "m") return unauthorized();
  if (await env.NUCLEIC_RELAY_TOKENS.get(`revoked:${v.claims.deviceId}`)) return new Response("revoked", { status: 403 });

  const claims = mintClaims({
    roomId: v.claims.roomId,
    deviceId: v.claims.deviceId,
    role: v.claims.role,
    typ: "c",
    ttlSeconds: CONNECTION_TTL,
  });
  return json({ token: await signToken(env.RELAY_TOKEN_SECRET, claims), exp: claims.exp });
}

/** Revoke a device on unpair (admin). KV TTL outlives the longest membership token. */
async function revoke(request: Request, env: Env): Promise<Response> {
  if (!adminOk(request, env)) return unauthorized();
  const body = (await safeJson(request)) as { deviceId?: string } | null;
  if (!body?.deviceId) return new Response("deviceId required", { status: 422 });
  await env.NUCLEIC_RELAY_TOKENS.put(`revoked:${body.deviceId}`, "1", { expirationTtl: MEMBERSHIP_TTL });
  return json({ revoked: body.deviceId });
}

// MARK: - Push (Phase C, out-of-band)

async function pushNotify(request: Request, env: Env): Promise<Response> {
  if (!adminOk(request, env)) return unauthorized();
  const cfg = apnsConfig(env);
  if (!cfg) return new Response("apns not configured", { status: 503 });

  const body = (await safeJson(request)) as { deviceId?: string } | null;
  if (!body?.deviceId) return new Response("deviceId required", { status: 422 });
  const rec = await env.NUCLEIC_PUSH_TOKENS.get(body.deviceId);
  if (!rec) return new Response("no push token for device", { status: 404 });

  const { token } = JSON.parse(rec) as { token: string };
  const result = await sendApns(cfg, token, approvalPayload());
  return json(result, result.status === 200 ? 200 : 502);
}

// MARK: - helpers

function method(request: Request, want: string, run: () => Promise<Response>): Promise<Response> | Response {
  if (request.method !== want) return new Response("method not allowed", { status: 405, headers: { allow: want } });
  return run();
}

function bearer(request: Request): string | null {
  const h = request.headers.get("authorization");
  const m = h && /^Bearer\s+(.+)$/i.exec(h);
  return m ? m[1] : null;
}

function adminOk(request: Request, env: Env): boolean {
  const want = env.RELAY_ADMIN_SECRET;
  const got = bearer(request);
  return !!want && !!got && safeEqual(got, want);
}

function safeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}

async function safeJson(request: Request): Promise<unknown> {
  try {
    return await request.json();
  } catch {
    return null;
  }
}

function json(obj: unknown, status = 200): Response {
  return new Response(JSON.stringify(obj), { status, headers: { "content-type": "application/json" } });
}

function unauthorized(): Response {
  return new Response("unauthorized", { status: 401 });
}
