// RunnerContainer Durable Object: one per container slot (`<poolId>/<instanceId>`), wrapping the
// Cloudflare Containers runtime attached to the DO (`ctx.container`). Deliberately thin — all
// policy (placement, caps, fencing, reaping) lives in the RunnerPool DO; this class only starts,
// stops, and proxies to its one container.
//
// The container image is containers/nucleic-runner (nucleic-sandbox + nucleicd). The raw
// `ctx.container` API is modeled structurally below because the pinned @cloudflare/workers-types
// predates it — one boundary cast, same precedent as the X25519 derive-params cast in enroll.ts.

import type { Env } from "./env.ts";

/** nucleicd's loopback control port inside the container: serves the one-shot pairing payload
 * and local health. Only ever reached through this DO's authenticated proxy, never exposed. */
export const NUCLEICD_CONTROL_PORT = 9200;

export interface ContainerController {
  running: boolean;
  start(opts?: { env?: Record<string, string>; enableInternet?: boolean }): void;
  monitor(): Promise<void>;
  destroy(reason?: string): Promise<void>;
  getTcpPort(port: number): { fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> };
}

/** Build the env a runner container boots with. Pure — unit-tested. */
export function buildContainerEnv(base: {
  poolId: string;
  instanceId: string;
  epoch: string;
  instanceToken: string;
  controlPlaneURL: string;
  relayURL?: string;
}): Record<string, string> {
  const env: Record<string, string> = {
    NUCLEIC_RUNNER_POOL_ID: base.poolId,
    NUCLEIC_RUNNER_INSTANCE_ID: base.instanceId,
    NUCLEIC_RUNNER_EPOCH: base.epoch,
    NUCLEIC_RUNNER_INSTANCE_TOKEN: base.instanceToken,
    NUCLEIC_RUNNER_CONTROL_URL: base.controlPlaneURL,
  };
  if (base.relayURL) env.NUCLEIC_RELAY_URL = base.relayURL;
  return env;
}

export class RunnerContainer implements DurableObject {
  private readonly container: ContainerController | undefined;

  constructor(state: DurableObjectState, _env: Env) {
    this.container = (state as unknown as { container?: ContainerController }).container;
  }

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    if (!this.container) {
      // Local `wrangler dev` without the Containers runtime, or a mis-bound namespace.
      return new Response("containers runtime unavailable", { status: 501 });
    }

    switch (`${request.method} ${url.pathname}`) {
      case "POST /start": {
        let body: { env?: Record<string, string> } | null = null;
        try {
          body = (await request.json()) as { env?: Record<string, string> };
        } catch {
          // start with no env is allowed
        }
        if (!this.container.running) {
          this.container.start({ env: body?.env ?? {}, enableInternet: true });
        }
        return new Response(JSON.stringify({ running: true }), {
          headers: { "content-type": "application/json" },
        });
      }
      case "POST /stop": {
        if (this.container.running) await this.container.destroy("pool stop");
        return new Response(JSON.stringify({ running: false }), {
          headers: { "content-type": "application/json" },
        });
      }
      case "GET /status":
        return new Response(JSON.stringify({ running: this.container.running }), {
          headers: { "content-type": "application/json" },
        });
      case "GET /pairing": {
        // One-shot provisioning proxy: nucleicd serves its PairingPayload exactly once on its
        // loopback control port; the Worker only routes this to a pool-credential holder.
        if (!this.container.running) return new Response("container not running", { status: 409 });
        return this.container.getTcpPort(NUCLEICD_CONTROL_PORT).fetch("http://container/pairing");
      }
      default:
        return new Response("not found", { status: 404 });
    }
  }
}
