import { readFileSync, existsSync, mkdirSync } from "node:fs";
import { createHash } from "node:crypto";
import { join, resolve, dirname } from "node:path";
import { contextModeAdapter } from "../../adapters/contextMode.ts";
import { readPid, isAlive } from "../../lifecycle/pid.ts";
import { paths } from "../../lifecycle/paths.ts";

function findProjectRoot(): string {
  let dir = dirname(resolve(process.argv[1]!));
  for (let i = 0; i < 5; i++) {
    if (existsSync(join(dir, "package.json"))) return dir;
    dir = dirname(dir);
  }
  throw new Error("fewtok project root not found (no package.json in parent chain)");
}

// Build candidate bytes to dist/proxy-handler.next.js — NEVER straight to live.
// The running child verifies the candidate (selfCheck) and only then atomically
// promotes it to dist/proxy-handler.js. Returns the path on success, null on failure.
// The candidate MUST end in .js: bun infers ESM vs CJS from the FINAL outfile
// extension, and an unrecognized ext (e.g. .next) silently emits CJS the server
// cannot load as named exports (--format=esm does NOT override this — verified no-op).
function buildProxyHandler(verbose: boolean): string | null {
  const root = findProjectRoot();
  const outDir = join(root, "dist");
  mkdirSync(outDir, { recursive: true });
  const nextFile = join(outDir, "proxy-handler.next.js");
  const result = Bun.spawnSync(
    [process.execPath, "build",
      join(root, "src", "proxy", "handler.ts"),
      "--target=bun",
      `--outfile=${nextFile}`],
    { cwd: root },
  );
  const ok = (result.exitCode ?? 1) === 0;
  if (!ok) {
    const msg = result.stderr.toString().slice(0, 600);
    if (verbose) { console.error("build failed:\n" + msg); } else { console.log("err=build-failed"); }
    return null;
  }
  return nextFile;
}

export async function cmdReload(verbose = false): Promise<number> {
  const pid = readPid(paths.pidFile());
  if (pid === null) {
    if (!verbose) { console.log("err=not-running"); } else { console.error("fewtok is not running (no pid file)"); }
    return 1;
  }
  if (!isAlive(paths.pidFile())) {
    if (!verbose) { console.log("err=not-running"); } else { console.error(`fewtok pid ${pid} is not alive`); }
    return 1;
  }

  // Build candidate bytes to dist/proxy-handler.next.js (never live).
  const nextFile = buildProxyHandler(verbose);
  if (nextFile === null) return 1;

  // Hash the candidate bytes so we can confirm THESE bytes went live. Promotion is
  // proven by a FRESH "[fewtok reload] handler swapped ... build=<hash>" log line
  // carrying this hash (emitted only after the child reassigns the live bundle); the
  // child's /healthz buildHash then narrows that it is actually serving the new bytes.
  // A /healthz hash match ALONE is NOT proof — on a no-op reload (unchanged source) it
  // equals the already-live hash immediately, even if the child's reload threw.
  let expected: string;
  try {
    expected = createHash("sha256").update(readFileSync(nextFile)).digest("hex").slice(0, 12);
  } catch (err) {
    if (!verbose) { console.log("err=hash-failed"); } else { console.error("failed to hash .next bundle:", err); }
    return 1;
  }

  // Send SIGUSR1 to trigger verify-then-promote hot-swap.
  try {
    process.kill(pid, "SIGUSR1");
  } catch (err) {
    if (!verbose) { console.log("err=reload-failed"); } else { console.error(`failed to send SIGUSR1 to pid ${pid}:`, err); }
    return 1;
  }

  const maybeRepairCtxMode = () => {
    if (contextModeAdapter.isInstalled() && contextModeAdapter.repair) {
      const r = contextModeAdapter.repair();
      if (verbose && r.symlinked.length > 0) {
        console.log(`ctx-mode: symlinked ${r.symlinked.length} version(s) → ${r.current}`);
      }
    }
  };

  // Poll /healthz for the promoted buildHash; fast-fail on a logged reload failure.
  const logPath = join(paths.logs(), `proxy-${new Date().toISOString().slice(0, 10)}.log`);
  const ok = await waitForReload(expected, logPath, 5000);
  maybeRepairCtxMode();
  if (ok) {
    if (!verbose) { console.log("ok"); } else { console.log(`fewtok reloaded (pid ${pid}, build=${expected})`); }
    return 0;
  }
  if (!verbose) { console.log("err=reload-unconfirmed"); } else { console.error(`reload not confirmed within 5s (expected build=${expected})`); }
  return 1;
}

// Confirm the reload from the child's log + /healthz. Success REQUIRES a FRESH
// "[fewtok reload] handler swapped ... build=<expected>" line (logged only after the
// live bundle is reassigned) AND /healthz reporting that same buildHash. Fast-fail the
// moment the child logs "[fewtok reload] failed" (bad build / selfCheck reject / rename
// error). Timeout → unconfirmed → non-zero. The swap-line gate is load-bearing: a
// /healthz hash match alone false-greens a no-op reload whose child actually threw.
async function waitForReload(expected: string, logPath: string, timeoutMs: number): Promise<boolean> {
  // Snapshot the log tail offset so we only react to lines emitted after our SIGUSR1.
  let offset = 0;
  try { offset = readFileSync(logPath, "utf8").length; } catch { /* log may not exist yet */ }

  const port = readPortFile();
  const deadline = Date.now() + timeoutMs;
  let swapped = false;
  while (Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, 100));

    let tail = "";
    try { tail = readFileSync(logPath, "utf8").slice(offset); } catch { /* log read errors — retry next tick */ }

    // Fast-fail: child logged a reload failure → live bytes unchanged, give up now.
    if (tail.includes("[fewtok reload] failed")) return false;

    // Promotion proof: the child logs "handler swapped ... build=<hash>" ONLY after it
    // has reassigned the live bundle to the new bytes. Gate on a FRESH (post-offset)
    // swap line carrying OUR hash — a /healthz hash match alone is insufficient because
    // a no-op reload (unchanged source) matches the already-live hash immediately, even
    // if the child's reload threw, which would false-green a broken bundle.
    if (!swapped && tail.includes("[fewtok reload] handler swapped") && tail.includes(`build=${expected}`)) {
      swapped = true;
    }

    // Narrow (never confirm alone): once swapped, verify /healthz is actually serving
    // those bytes before declaring success.
    if (swapped && port !== null) {
      try {
        // Connection: close → a fresh TCP connection per poll. Under N=2 + reusePort,
        // keep-alive would pin every poll to ONE listener; if that is the not-yet-rolled
        // secondary (still old bytes) we'd never observe the promoted hash within the
        // deadline and false-report "unconfirmed". Fresh connections let the kernel
        // redistribute so we reach the already-swapped primary.
        const res = await fetch(`http://127.0.0.1:${port}/healthz`, {
          signal: AbortSignal.timeout(500),
          headers: { Connection: "close" },
        });
        if (res.ok) {
          const body = (await res.json()) as { buildHash?: string };
          if (body.buildHash === expected) return true;
        }
      } catch { /* transient — keep polling until deadline */ }
    }
  }
  return false;
}

function readPortFile(): number | null {
  try {
    const raw = readFileSync(paths.portFile(), "utf8").trim();
    const n = Number.parseInt(raw, 10);
    return Number.isFinite(n) && n > 0 ? n : null;
  } catch {
    return null;
  }
}
