// 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, proxies to its one container, and keeps itself resident while the container runs (the
// keepalive alarm below).
//
// 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 per-call one-time pairing
 * payloads and local health. Only ever reached through this DO's authenticated proxy, never
 * exposed. */
export const NUCLEICD_CONTROL_PORT = 9200;

/** How often the DO's alarm re-fires to renew its own activity while the container runs. A
 * container's lifetime is tied to its Durable Object, and a DO is evicted after 70–140 s
 * without events — taking the container down with SIGTERM. This DO gets no traffic after
 * `/start` (+ one `/pairing` read); heartbeats go to the *pool* DO — so without a self-renewing
 * alarm every runner died ~2 minutes after boot (the 2026-07 "runner dies mid-provision"
 * incidents). The alarm firing is itself the activity that staves off eviction, so the cadence
 * MUST sit well inside the 70 s eviction floor (asserted in container.test.ts). The pool's
 * heartbeat-staleness / idle reaping (pool.ts) remains the thing that decides to STOP a runner. */
export const KEEPALIVE_INTERVAL_MS = 30_000;

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;
  /** How nucleicd replaces AFMs (COVALENCE_RUNNER §5, item 6): "mesh" | "agent" |
   * "heuristic", from the pool settings. Omitted ⇒ nucleicd's own default (mesh). */
  intelligenceMode?: 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,
    // Explicit mesh display name: a Cloudflare container is always a cloud runner, so say so
    // from the control plane rather than relying on nucleicd's headless-host fallback (which
    // once mislabeled these "Mac Runner" when Linux's machine name came back nil).
    NUCLEIC_RUNNER_NAME: "Cloud Runner",
  };
  if (base.relayURL) env.NUCLEIC_RELAY_URL = base.relayURL;
  if (base.intelligenceMode) env.NUCLEIC_RUNNER_INTELLIGENCE_MODE = base.intelligenceMode;
  return env;
}

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

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

  /** Keepalive (see KEEPALIVE_INTERVAL_MS): re-arm while the container runs; once it has
   * stopped — pool stop or its own exit — let the chain lapse so the DO can idle out. */
  async alarm(): Promise<void> {
    if (this.container?.running) {
      await this.state.storage.setAlarm(Date.now() + KEEPALIVE_INTERVAL_MS);
    }
  }

  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 });
        }
        await this.state.storage.setAlarm(Date.now() + KEEPALIVE_INTERVAL_MS);
        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");
        await this.state.storage.deleteAlarm();
        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": {
        // Provisioning proxy: nucleicd mints a fresh one-time PairingPayload per call on its
        // 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 });
    }
  }
}
