#!/usr/bin/env node

/**
 * `rastack dev` — local development, always over the WASM engine.
 *
 *   rastack dev [resourcesDir] [--port <n>] [--warehouse <dir>] [--out <dir>] [--no-open]
 *
 * Compiles the resources, then boots a dependency-free static server (à la
 * `rastack design`) that hands the browser everything it needs to run the app +
 * admin locally over WebAssembly — no native server, no cargo:
 *   • the bundled React admin (`/dev/admin.js`),
 *   • the prebuilt WASM engine (`/wasm/…`) shipped in the package,
 *   • the compiled `schema.rastack.json` / `openapi.json`,
 *   • the committed Iceberg warehouse (`/data/warehouse/…`) to seed from.
 *
 * The engine is resolved from the package's prebuilt bundle; if none is present
 * but a Rust + wasm toolchain is, it is built from source as a fallback.
 */

import { execFileSync, spawnSync } from "child_process";
import * as fs from "fs";
import * as http from "http";
import * as path from "path";
import {
  WASM_ENTRY,
  contentType,
  parseDevArgs,
  renderShell,
  wasmBundleCandidates,
} from "./dev/harness";

const HERE = __dirname; // dist/ when installed

/** Compile the resources to schema.rastack.json + openapi.json in outDir. */
function compile(resourcesDir: string, outDir: string): boolean {
  const compiler = path.join(HERE, "rastack-compile.js");
  const result = spawnSync(
    process.execPath,
    [compiler, "compile", resourcesDir, outDir],
    { stdio: "inherit" },
  );
  if (result.status !== 0) return false;
  return fs.existsSync(path.join(outDir, "schema.rastack.json"));
}

/** True when `cargo` is on PATH (the from-source wasm fallback needs it). */
function cargoAvailable(): boolean {
  try {
    return (
      spawnSync("cargo", ["--version"], { stdio: "ignore" }).status === 0
    );
  } catch {
    return false;
  }
}

/** Locate the prebuilt wasm bundle dir, building from source as a fallback. */
function resolveWasmDir(): string | null {
  for (const dir of wasmBundleCandidates(HERE)) {
    if (fs.existsSync(path.join(dir, WASM_ENTRY))) return dir;
  }
  if (cargoAvailable()) {
    console.log(
      "\n▸ No prebuilt WASM engine found — building it from source (cargo)…\n",
    );
    try {
      // Lazy require so a normal `dev` with a prebuilt bundle never loads it.
      // eslint-disable-next-line @typescript-eslint/no-var-requires
      require("./rastack-wasm-build").buildWasm();
    } catch (err) {
      console.error(`  ✗ wasm build failed: ${(err as Error).message}`);
      return null;
    }
    for (const dir of wasmBundleCandidates(HERE)) {
      if (fs.existsSync(path.join(dir, WASM_ENTRY))) return dir;
    }
  }
  return null;
}

/** Serve one file if it exists and stays within `root`; else 404/403. */
function serveUnder(
  res: http.ServerResponse,
  root: string,
  relPath: string,
): void {
  const safeRoot = path.resolve(root);
  const rel = decodeURIComponent(relPath).replace(/^\/+/, "");
  const target = path.resolve(safeRoot, rel);
  // Path-traversal guard: the resolved target must stay inside the root.
  if (target !== safeRoot && !target.startsWith(safeRoot + path.sep)) {
    res.writeHead(403).end("Forbidden");
    return;
  }
  fs.readFile(target, (err, data) => {
    if (err) {
      res.writeHead(404).end("Not found");
      return;
    }
    res.writeHead(200, { "Content-Type": contentType(target) }).end(data);
  });
}

/** Serve an exact file (no traversal concern — a fixed internal artifact). */
function serveFile(res: http.ServerResponse, file: string): void {
  fs.readFile(file, (err, data) => {
    if (err) {
      res.writeHead(404).end("Not found");
      return;
    }
    res.writeHead(200, { "Content-Type": contentType(file) }).end(data);
  });
}

/** Best-effort open the default browser at `url`. */
function openBrowser(url: string): void {
  const cmd =
    process.platform === "darwin"
      ? "open"
      : process.platform === "win32"
        ? "start"
        : "xdg-open";
  try {
    execFileSync(cmd, [url], { stdio: "ignore" });
  } catch {
    /* non-fatal — the URL is printed anyway */
  }
}

function main(argv: string[]): void {
  const args = parseDevArgs(argv);

  console.log(`\n  rastack dev — the app + admin, in your browser over WASM\n`);
  console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
  if (!compile(args.resourcesDir, args.outDir)) {
    console.error("  ✗ compile failed — fix the resource graph and re-run.");
    process.exit(1);
  }

  const wasmDir = resolveWasmDir();
  if (!wasmDir) {
    console.error(
      "\n  ✗ No WASM engine available.\n" +
        "    Upgrade rastack for the prebuilt bundle (`rastack update`), or install\n" +
        "    the Rust toolchain to build it from source:\n" +
        "      rustup target add wasm32-unknown-unknown\n" +
        "      cargo install wasm-bindgen-cli\n",
    );
    process.exit(1);
  }

  const adminBundle = path.join(HERE, "admin.js");
  if (!fs.existsSync(adminBundle)) {
    console.error(
      "  ✗ admin bundle (dist/admin.js) not found — reinstall rastack.",
    );
    process.exit(1);
  }

  const server = http.createServer((req, res) => {
    const url = (req.url || "/").split("?")[0];
    if (req.method !== "GET") {
      res.writeHead(405).end("Method not allowed");
      return;
    }
    if (url === "/" || url === "/index.html") {
      res
        .writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
        .end(renderShell());
      return;
    }
    if (url === "/dev/admin.js") return serveFile(res, adminBundle);
    if (url.startsWith("/wasm/"))
      return serveUnder(res, wasmDir, url.slice("/wasm/".length));
    if (url === "/schema.rastack.json" || url === "/openapi.json")
      return serveUnder(res, args.outDir, path.basename(url));
    if (url.startsWith("/data/warehouse/"))
      return serveUnder(
        res,
        args.warehouseDir,
        url.slice("/data/warehouse/".length),
      );
    res.writeHead(404).end("Not found");
  });

  server.listen(args.port, () => {
    const url = `http://localhost:${args.port}`;
    console.log(`  ✓ WASM engine: ${path.relative(process.cwd(), wasmDir)}`);
    console.log(`\n  ▸ ${url}   (admin at ${url}/#/)\n`);
    console.log("  Press Ctrl+C to stop.\n");
    if (args.open) openBrowser(url);
  });
}

if (require.main === module) {
  main(process.argv.slice(2));
}
