// The dev host (Vision §5.1, K3) — the local environment a plugin developer runs.
// Boots ONE OR MORE plugins on Bun, each on its own SQLite + synthetic logged-in
// admin (via the preload), behind a tiny "dev kernel" mediator that stands in for
// the production kernel's `/_internal/rpc` gateway so cross-plugin calls work — and
// **degrade gracefully** when a peer isn't loaded (the resilience rule: a missing
// dependency shows an error, it never crashes the app).
//
//   bun packages/plugin-sdk/src/dev/dev-host.ts <pluginDir> [<pluginDir> …]
//
// Each plugin's own `server/main.ts` is byte-identical to prod — it just finds the
// dev hooks installed by the preload and runs on the local engine. Loading several
// at once lets a developer exercise the CONTRACT between plugins (does B expose the
// method A consumes?) entirely locally, no kernel, no Postgres.

import { readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { spawn, type Subprocess } from "bun";
import { handleDevAsset } from "./dev-assets.js";

const DEV_SECRET = "dev-stub-internal-secret";
const BASE_PORT = Number(process.env.KSERP_DEV_BASE_PORT || "4500");
const PRELOAD = join(import.meta.dir, "preload.ts");

interface Manifest {
  name: string;
  schemas?: string[];
}
interface Loaded {
  dir: string;
  name: string;
  port: number;
  proc: Subprocess;
  status: "starting" | "up" | "failed";
}

const dirs = process.argv.slice(2).map((d) => resolve(d));
if (dirs.length === 0) {
  console.error("usage: bun dev-host.ts <pluginDir> [<pluginDir> …]");
  process.exit(1);
}

// ── assign ports + read manifests (the mediator routes by plugin name → port) ──
const mediatorPort = BASE_PORT;
const registry = new Map<string, number>();
const loaded: Loaded[] = [];
dirs.forEach((dir, i) => {
  const manifest = JSON.parse(readFileSync(join(dir, "plugin.manifest.json"), "utf8")) as Manifest;
  const port = BASE_PORT + 1 + i;
  registry.set(manifest.name, port);
  loaded.push({ dir, name: manifest.name, port, proc: undefined as never, status: "starting" });
});

// Answer the api.assets relay (`/_internal/assets/*`) against local MinIO + an in-memory
// ledger, clamped to the active (synthetic) identity the relay forwarded in x-kserp-identity.
// Returns null for a non-asset path. Extracted from the mediator's fetch to keep that handler
// within its complexity budget.
async function tryAssetRelay(pathname: string, req: Request): Promise<Response | null> {
  if (!pathname.startsWith("/_internal/assets/")) return null;
  let identity = { workspaceId: 1, ownerId: "dev-admin" };
  const idHdr = req.headers.get("x-kserp-identity");
  if (idHdr) {
    try {
      const v = JSON.parse(idHdr) as { workspaceId?: number; userId?: string };
      identity = {
        workspaceId: Number(v.workspaceId) || 1,
        ownerId: String(v.userId ?? "dev-admin"),
      };
    } catch {
      /* malformed → fall back to the default synthetic admin */
    }
  }
  return handleDevAsset(pathname, req, identity);
}

// ── the dev-kernel mediator: stands in for the prod kernel's /_internal/rpc. Routes
//    a cross-plugin call to the target's /_internal/services/<method>; a peer that
//    isn't loaded → 503, which the SDK's tryCallPlugin turns into a graceful null and
//    callPlugin into a thrown error the consumer's route reports (never a crash). ──
const mediator = Bun.serve({
  port: mediatorPort,
  async fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === "/_internal/rpc" && req.method === "POST") {
      const body = (await req.json()) as { target: string; method: string; args: unknown };
      const targetPort = registry.get(body.target);
      if (!targetPort) {
        return Response.json(
          { error: `plugin "${body.target}" is not loaded in this dev session` },
          { status: 503 },
        );
      }
      try {
        const upstream = await fetch(
          `http://127.0.0.1:${targetPort}/_internal/services/${encodeURIComponent(body.method)}`,
          {
            method: "POST",
            headers: {
              "content-type": "application/json",
              "x-kserp-internal": DEV_SECRET,
              ...(req.headers.get("x-kserp-identity")
                ? (req.headers.get("x-kserp-identity")!.includes(".") 
                     ? { "x-kserp-identity": req.headers.get("x-kserp-identity")! }
                     : { "x-kserp-dev-identity": req.headers.get("x-kserp-identity")! })
                : {}),
            },
            body: JSON.stringify(body.args ?? {}),
          },
        );
        return new Response(await upstream.text(), {
          status: upstream.status,
          headers: { "content-type": "application/json" },
        });
      } catch {
        // target loaded but unreachable (still booting / crashed) → 503 = degrade.
        return Response.json({ error: `plugin "${body.target}" unreachable` }, { status: 503 });
      }
    }
    // api.assets (A1) in the toolkit: a plugin relays upload/presign/delete here.
    const asset = await tryAssetRelay(url.pathname, req);
    if (asset) return asset;
    if (url.pathname === "/" || url.pathname === "/_dev/status") {
      return Response.json({
        mediator: `http://127.0.0.1:${mediatorPort}`,
        plugins: loaded.map((l) => ({ name: l.name, port: l.port, status: l.status })),
      });
    }
    return new Response("dev-kernel", { status: 404 });
  },
});

// ── spawn each plugin (Bun + preload → SQLite + synthetic admin) ──
for (const l of loaded) {
  const manifest = JSON.parse(
    readFileSync(join(l.dir, "plugin.manifest.json"), "utf8"),
  ) as Manifest;
  l.proc = spawn({
    cmd: ["bun", "--preload", PRELOAD, join(l.dir, "server", "main.ts")],
    cwd: l.dir,
    env: {
      ...process.env,
      KSERP_DEV_PLUGIN_DIR: l.dir,
      KSERP_DB_ENGINE: "sqlite",
      KSERP_PLUGIN_PORT: String(l.port),
      KSERP_PLUGIN_BIND: "127.0.0.1",
      KSERP_PLUGIN_SCHEMAS: (manifest.schemas ?? []).join(","),
      KSERP_KERNEL_URL: `http://127.0.0.1:${mediatorPort}`,
      KSERP_INTERNAL_SECRET: DEV_SECRET,
    },
    stdout: "inherit",
    stderr: "inherit",
  });
}

// ── wait for health, report (a plugin whose own boot throws — e.g. a peer-dependent
//    migration — is reported failed, not fatal to the host) ──
async function waitHealthy(l: Loaded, timeoutMs = 15000): Promise<void> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try {
      const r = await fetch(`http://127.0.0.1:${l.port}/_internal/health`);
      if (r.ok) {
        l.status = "up";
        return;
      }
    } catch {
      /* not up yet */
    }
    if (l.proc.exitCode !== null) {
      l.status = "failed";
      return;
    }
    await Bun.sleep(300);
  }
  l.status = l.proc.exitCode === null ? "up" : "failed";
}

await Promise.all(loaded.map((l) => waitHealthy(l)));

console.log("\n────────────────────────────────────────────────────────");
console.log(`  dev kernel   http://127.0.0.1:${mediatorPort}  (cross-plugin RPC mediator)`);
for (const l of loaded) {
  const mark = l.status === "up" ? "✓" : "✗";
  console.log(`  ${mark} ${l.name.padEnd(20)} http://127.0.0.1:${l.port}  [${l.status}]`);
}
console.log("────────────────────────────────────────────────────────");
console.log(
  `  ${loaded.filter((l) => l.status === "up").length}/${loaded.length} plugins up. Ctrl-C to stop.\n`,
);

function shutdown() {
  for (const l of loaded) l.proc.kill();
  mediator.stop(true);
  process.exit(0);
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
