// §5.3 — the FULL toolkit launcher. Boots N plugins + the dev-kernel (the complete
// `/api` + `/_internal` backend) AND serves the prebuilt kernel-identical host-UI
// SHELL, so `npx @kahitsan/plugin-sdk <dir>…` brings up the real visual app with ZERO
// kserp source. The shell is a static SPA (the host UI is `ssr:false`, so a toolkit
// `vinxi build` emits pure static files) shipped in the package at `<pkg>/shell`.
//
//   bun packages/plugin-sdk/src/dev/serve.ts <pluginDir> [<pluginDir> …]
//
// Topology: the browser only ever talks to THIS server (one origin). Static asset →
// served from ./shell; `/api/*` or `/_internal/*` → proxied to the dev-kernel (which
// itself proxies `<basePath>/_ui/remote.js` to each plugin); anything else → index.html
// (the SPA owns client routing). dev-host.ts remains the headless/backend-only variant.

import { readFileSync, existsSync } from "node:fs";
import { join, resolve } from "node:path";
import { spawn, type Subprocess } from "bun";
import { readPluginRegistrations, shutdownToolkit, type PluginRegistration } from "./serve-config.ts";

const PRELOAD = join(import.meta.dir, "preload.ts");
const DEV_KERNEL = join(import.meta.dir, "dev-kernel.ts");
// The prebuilt static shell: `<pkg>/shell` (src/dev → ../../shell). Built by
// `npm run build:toolkit-shell`; shipped via package.json `files`.
const SHELL_DIR = resolve(import.meta.dir, "../../shell");
const DEV_SECRET = "dev-stub-internal-secret";
const BASE = Number(process.env.KSERP_DEV_BASE_PORT || "4500");

const dirs = process.argv.slice(2).map((d) => resolve(d));
if (dirs.length === 0) {
  console.error("usage: bun serve.ts <pluginDir> [<pluginDir> …]");
  process.exit(1);
}
if (!existsSync(join(SHELL_DIR, "index.html"))) {
  console.error(
    `[toolkit] host-UI shell missing at ${SHELL_DIR}.\n` +
      `  In a source checkout run \`npm run build:toolkit-shell\`; in an installed package this\n` +
      `  means the published tarball didn't ship ./shell — reinstall @kahitsan/plugin-sdk.`,
  );
  process.exit(1);
}

const uiPort = BASE; // the visual app the developer opens
const kernelPort = BASE + 1; // dev-kernel: /api + /_internal
const KERNEL = `http://127.0.0.1:${kernelPort}`;

const regs: PluginRegistration[] = readPluginRegistrations(dirs, BASE);

// ── spawn each plugin (Bun + preload → SQLite + synthetic admin), pointed at the dev-kernel ──
const procs: Subprocess[] = [];
for (const r of regs) {
  const m = JSON.parse(readFileSync(join(r.dir, "plugin.manifest.json"), "utf8")) as {
    schemas?: string[];
  };
  procs.push(
    spawn({
      cmd: ["bun", "--preload", PRELOAD, join(r.dir, "server", "main.ts")],
      cwd: r.dir,
      env: {
        ...process.env,
        KSERP_DEV_PLUGIN_DIR: r.dir,
        KSERP_DB_ENGINE: "sqlite",
        KSERP_PLUGIN_PORT: String(r.port),
        KSERP_PLUGIN_BIND: "127.0.0.1",
        KSERP_PLUGIN_SCHEMAS: (m.schemas ?? []).join(","),
        KSERP_KERNEL_URL: KERNEL,
        KSERP_INTERNAL_SECRET: DEV_SECRET,
      },
      stdout: "inherit",
      stderr: "inherit",
    }),
  );
}

// ── spawn the dev-kernel (serves /api/* + /_internal/rpc + /_internal/assets, and proxies
//    each plugin's bundle + routes by basePath) ──
const kernelProc = spawn({
  cmd: ["bun", DEV_KERNEL],
  env: {
    ...process.env,
    KSERP_DEV_KERNEL_PORT: String(kernelPort),
    KSERP_INTERNAL_SECRET: DEV_SECRET,
    KSERP_DEV_PLUGINS: JSON.stringify(
      regs.map((r) => ({ name: r.name, dir: r.dir, port: r.port })),
    ),
  },
  stdout: "inherit",
  stderr: "inherit",
});

// ── the one origin the browser talks to: static shell + a proxy to the dev-kernel ──
const ui = Bun.serve({
  port: uiPort,
  async fetch(req) {
    const url = new URL(req.url);
    const path = url.pathname;
    // backend (kernel API, cross-plugin RPC, assets, AND plugin bundles under /api/<base>/_ui)
    if (path === "/api" || path.startsWith("/api/") || path.startsWith("/_internal/")) {
      const upstream = await fetch(KERNEL + path + url.search, {
        method: req.method,
        headers: req.headers,
        body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
        redirect: "manual",
      }).catch(() => null);
      if (!upstream) return new Response("dev-kernel not ready", { status: 502 });
      return new Response(upstream.body, { status: upstream.status, headers: upstream.headers });
    }
    // a real static asset (index.html, /_build/*, icons, /dev/*)
    const rel = path === "/" ? "/index.html" : path;
    const asset = Bun.file(join(SHELL_DIR, rel));
    if (await asset.exists()) return new Response(asset);
    // otherwise it's a client route (/payees, /transactions, …) → the SPA owns it
    return new Response(Bun.file(join(SHELL_DIR, "index.html")), {
      headers: { "content-type": "text/html" },
    });
  },
});

console.log("\n────────────────────────────────────────────────────────");
console.log(`  ▸ toolkit app   http://127.0.0.1:${ui.port}   ← open this`);
console.log(`  ▸ dev-kernel    http://127.0.0.1:${kernelPort}  (/api + /_internal)`);
for (const r of regs) console.log(`  ▸ ${r.name.padEnd(18)} :${r.port}`);
console.log("────────────────────────────────────────────────────────");
console.log("  The kernel-identical shell is served from the package — no kserp source needed.");
console.log("  Ctrl-C stops everything.\n");

function shutdown() {
  shutdownToolkit(procs, kernelProc, () => ui.stop(true));
  process.exit(0);
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
