// nucleic-runner Worker: the Antimatter runner control plane (docs/ANTIMATTER_RUNNER.md §8).
//
// Auth model, two credentials:
//   • pool credential `poolId.poolSecret` — minted at PoP enrollment (enroll.ts), held by the
//     user's devices, presented by Settings ▸ Antimatter ▸ Runner. Grants pool management.
//   • instance token — HMAC-signed (runnerToken.ts), injected into each container at boot,
//     re-minted on every heartbeat. Grants exactly the pool-plumbing verbs a container needs.
//   • RUNNER_ADMIN_SECRET is the operator override (inspection/revocation), like the relay's.
//
// The Worker authenticates, resolves the poolId, and forwards to that pool's Durable Object
// with `x-nucleic-auth` stamped — the DO trusts only that header, never raw bearers.

import type { Env } from "./env.ts";
import { authedPool, enrollChallenge, enrollComplete, POOL_AUTH_TTL, sha256Hex } from "./enroll.ts";
import { INSTANCE_TTL, mintClaims, signToken, verifyToken } from "./runnerToken.ts";
import { RunnerPoolCore, type ContainerDriver, type PoolStateLike } from "./pool.ts";
import { buildContainerEnv, RunnerContainer } from "./container.ts";
import { handleWakeHook, registerRoomMapping } from "./wake.ts";

export { RunnerContainer };

function bearer(request: Request): string | null {
  const h = request.headers.get("authorization");
  if (!h?.startsWith("Bearer ")) return null;
  return h.slice("Bearer ".length).trim();
}

function isAdmin(env: Env, token: string | null): boolean {
  return Boolean(env.RUNNER_ADMIN_SECRET && token && token === env.RUNNER_ADMIN_SECRET);
}

async function readJson(request: Request): Promise<Record<string, unknown> | null> {
  try {
    return (await request.json()) as Record<string, unknown>;
  } catch {
    return null;
  }
}

/** Resolve which pool a management request may act on: pool credential wins; the operator
 * override must name the pool explicitly (`?pool=`). */
async function resolveManagedPool(env: Env, request: Request, url: URL): Promise<string | null> {
  const token = bearer(request);
  if (isAdmin(env, token)) return url.searchParams.get("pool");
  return authedPool(env.NUCLEIC_RUNNER_TOKENS, token);
}

function poolStub(env: Env, poolId: string): DurableObjectStub {
  return env.NUCLEIC_RUNNER_POOLS.get(env.NUCLEIC_RUNNER_POOLS.idFromName(poolId));
}

async function forwardToPool(
  env: Env,
  poolId: string,
  path: string,
  init: { method: string; auth: string; body?: string },
): Promise<Response> {
  return poolStub(env, poolId).fetch(`https://pool${path}`, {
    method: init.method,
    headers: {
      "x-nucleic-auth": init.auth,
      "x-nucleic-pool": poolId,
      ...(init.body !== undefined ? { "content-type": "application/json" } : {}),
    },
    body: init.body,
  });
}

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");

    // --- Pool enrollment (X25519 PoP; open like the relay's — possession is the gate) ---
    if (request.method === "POST" && pathname === "/v1/runner/enroll/challenge") {
      return enrollChallenge(env.NUCLEIC_RUNNER_TOKENS, await readJson(request));
    }
    if (request.method === "POST" && pathname === "/v1/runner/enroll") {
      return enrollComplete(env.NUCLEIC_RUNNER_TOKENS, await readJson(request));
    }

    // --- Operator revocation ---
    if (request.method === "POST" && pathname === "/v1/pool/revoke") {
      if (!isAdmin(env, bearer(request))) return new Response("unauthorized", { status: 401 });
      const body = await readJson(request);
      if (typeof body?.poolId !== "string") return new Response("poolId required", { status: 422 });
      await env.NUCLEIC_RUNNER_TOKENS.put(`revoked:${body.poolId}`, "1", { expirationTtl: POOL_AUTH_TTL });
      await env.NUCLEIC_RUNNER_TOKENS.delete(`poolauth:${body.poolId}`);
      return new Response(JSON.stringify({ ok: true }), { headers: { "content-type": "application/json" } });
    }

    // --- Relay presence → wake hook (docs/ANTIMATTER_RUNNER.md §3, item 3e) ---
    // The relay's Room DO calls this when a client connects to a room with no host present;
    // the room→pool mapping below is registered by host heartbeats (wake.ts).
    if (request.method === "POST" && pathname === "/v1/hook/wake") {
      return handleWakeHook({
        kv: env.NUCLEIC_RUNNER_TOKENS,
        secret: env.RUNNER_WAKE_SECRET,
        token: bearer(request),
        body: await readJson(request),
        forward: (poolId) => forwardToPool(env, poolId, "/wake", { method: "POST", auth: "pool" }),
      });
    }

    // --- Instance-token routes (containers phoning home) ---
    if (
      request.method === "POST" &&
      (pathname === "/v1/pool/heartbeat" ||
        pathname === "/v1/pool/sandbox/acquire" ||
        pathname === "/v1/pool/sandbox/release")
    ) {
      const secret = env.RUNNER_TOKEN_SECRET;
      if (!secret) return new Response("token secret unset", { status: 500 });
      const token = bearer(request);
      if (!token) return new Response("unauthorized", { status: 401 });
      const verdict = await verifyToken(secret, token);
      if (!verdict.ok) return new Response(`unauthorized: ${verdict.error}`, { status: 401 });
      if (await env.NUCLEIC_RUNNER_TOKENS.get(`revoked:${verdict.claims.poolId}`)) {
        return new Response("unauthorized: revoked", { status: 401 });
      }
      const bodyText = await request.text();
      if (pathname === "/v1/pool/heartbeat") {
        // A host heartbeat carries the runner's relay roomId — refresh the room→pool record
        // the wake hook resolves. Tolerant of a missing/malformed body (older nucleicd).
        let parsed: Record<string, unknown> | null = null;
        try {
          parsed = JSON.parse(bodyText) as Record<string, unknown>;
        } catch {
          parsed = null;
        }
        await registerRoomMapping(
          env.NUCLEIC_RUNNER_TOKENS, verdict.claims.poolId, verdict.claims.instanceId, parsed);
      }
      const subPath = pathname.replace("/v1/pool", "");
      return forwardToPool(env, verdict.claims.poolId, subPath, {
        method: "POST",
        auth: `instance:${verdict.claims.instanceId}`,
        body: bodyText,
      });
    }

    // --- Pool-credential routes (Settings / provisioning device) ---
    if (pathname.startsWith("/v1/pool/")) {
      const poolId = await resolveManagedPool(env, request, url);
      if (!poolId) return new Response("unauthorized", { status: 401 });

      switch (`${request.method} ${pathname}`) {
        case "GET /v1/pool/status":
          return forwardToPool(env, poolId, "/status", { method: "GET", auth: "pool" });
        case "PATCH /v1/pool/settings":
          return forwardToPool(env, poolId, "/settings", {
            method: "PATCH",
            auth: "pool",
            body: await request.text(),
          });
        case "POST /v1/pool/provision":
          return forwardToPool(env, poolId, "/provision", { method: "POST", auth: "pool" });
        case "POST /v1/pool/wake":
          return forwardToPool(env, poolId, "/wake", { method: "POST", auth: "pool" });
        case "POST /v1/pool/stop":
          return forwardToPool(env, poolId, "/stop", { method: "POST", auth: "pool" });
        case "GET /v1/pool/pairing": {
          // Provisioning proxy straight to the host container's nucleicd (one-shot there).
          const id = env.NUCLEIC_RUNNER_CONTAINERS.get(env.NUCLEIC_RUNNER_CONTAINERS.idFromName(`${poolId}/host`));
          return id.fetch("https://container/pairing");
        }
        default:
          return new Response("not found", { status: 404 });
      }
    }

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

// --- RunnerPool: the Workers-runtime binding over RunnerPoolCore ------------------------------

/** Drives container slots through the NUCLEIC_RUNNER_CONTAINERS namespace. Env injection happens at
 * start; the control-plane URL is this Worker's public origin so containers can phone home. */
function makeDriver(env: Env): ContainerDriver {
  return {
    async start(poolId, instanceId, extraEnv) {
      const stub = env.NUCLEIC_RUNNER_CONTAINERS.get(env.NUCLEIC_RUNNER_CONTAINERS.idFromName(`${poolId}/${instanceId}`));
      const containerEnv = buildContainerEnv({
        poolId,
        instanceId,
        epoch: extraEnv.NUCLEIC_RUNNER_EPOCH ?? "0",
        instanceToken: extraEnv.NUCLEIC_RUNNER_INSTANCE_TOKEN ?? "",
        controlPlaneURL: "https://runner.nucleic.blakeslee.xyz",
      });
      const res = await stub.fetch("https://container/start", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ env: containerEnv }),
      });
      if (!res.ok) throw new Error(`container start failed: ${res.status}`);
    },
    async stop(poolId, instanceId) {
      const stub = env.NUCLEIC_RUNNER_CONTAINERS.get(env.NUCLEIC_RUNNER_CONTAINERS.idFromName(`${poolId}/${instanceId}`));
      await stub.fetch("https://container/stop", { method: "POST" });
    },
  };
}

export class RunnerPool implements DurableObject {
  private core: RunnerPoolCore | undefined;
  private readonly state: DurableObjectState;
  private readonly env: Env;

  constructor(state: DurableObjectState, env: Env) {
    this.state = state;
    this.env = env;
  }

  /** The DO learns its poolId from the first request's header and pins it in storage so
   * alarm() (which has no request) can rebuild the core after hibernation. */
  private async coreFor(poolId: string | null): Promise<RunnerPoolCore | null> {
    if (this.core) return this.core;
    let pinned = await this.state.storage.get<string>("poolId");
    if (!pinned && poolId) {
      pinned = poolId;
      await this.state.storage.put("poolId", pinned);
    }
    if (!pinned) return null;
    const env = this.env;
    this.core = new RunnerPoolCore(this.state as unknown as PoolStateLike, pinned, {
      driver: makeDriver(env),
      mintInstanceToken: async (pool, instance) => {
        const secret = env.RUNNER_TOKEN_SECRET;
        if (!secret) throw new Error("RUNNER_TOKEN_SECRET unset");
        return signToken(secret, mintClaims({ poolId: pool, instanceId: instance, ttlSeconds: INSTANCE_TTL }));
      },
    });
    return this.core;
  }

  async fetch(request: Request): Promise<Response> {
    const core = await this.coreFor(request.headers.get("x-nucleic-pool"));
    if (!core) return new Response("pool unresolved", { status: 500 });
    return core.fetch(request);
  }

  async alarm(): Promise<void> {
    const core = await this.coreFor(null);
    await core?.alarm();
  }
}

// sha256Hex re-exported for tests that pin the poolId derivation against the Swift side.
export { sha256Hex };
