import { execFile, spawn } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import {
  acquireApplyLock,
  appendBoundedLog,
  assertCanonicalSourceRoot,
  copySourceTree,
  pwaAllowedHostsEnvironment,
  promoteRelease,
  redactOperationalError,
  rollbackRelease,
  runtimeHealthUrls,
  safeApplyWaitError,
  windowsCommand
} from "./safe-runtime-lib.mjs";

const exec = promisify(execFile);
const delay = (ms) => new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
const safeHome = resolve(process.env.CODE_REMOTE_MANAGER_SAFE_HOME || join(process.env.LOCALAPPDATA || process.cwd(), "CodeRemoteManager", "safe-runtime"));
const controlDir = join(safeHome, "control");
const current = join(safeHome, "current");
const staging = join(safeHome, "staging");
const previous = join(safeHome, "previous");
const failed = join(safeHome, "failed");
const logsDir = join(safeHome, "logs");
const requestPath = join(controlDir, "apply-request.json");
const resultPath = join(controlDir, "apply-result.json");
const applyLockPath = join(controlDir, "apply.lock");

function argument(name, fallback) {
  const index = process.argv.indexOf(`--${name}`);
  return index >= 0 ? process.argv[index + 1] : fallback;
}

async function run(command, args, cwd) {
  const invocation = command.endsWith(".cmd") ? windowsCommand(command, args) : { command, args };
  await exec(invocation.command, invocation.args, { cwd, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
}

async function verify(sourceRoot) {
  await run("npm.cmd", ["test"], sourceRoot);
  await run("npm.cmd", ["--workspace", "apps/pwa", "exec", "vite", "build"], sourceRoot);
}

async function verifySourceDiff(sourceRoot) {
  await run("git.exe", ["diff", "--check"], sourceRoot);
}

async function prepare(sourceRoot) {
  await copySourceTree(sourceRoot, staging);
  await run("npm.cmd", ["ci", "--ignore-scripts"], staging);
}

async function install() {
  const sourceRoot = resolve(argument("source", process.cwd()));
  const configPath = join(sourceRoot, "config.local.json");
  await readFile(configPath);
  await mkdir(controlDir, { recursive: true });
  await verifySourceDiff(sourceRoot);
  await prepare(sourceRoot);
  await verify(staging);
  await promoteRelease({ safeHome, current, staging, previous });
  await writeFile(join(controlDir, "settings.json"), JSON.stringify({ configPath }, null, 2));
  await writeFile(join(controlDir, "safe-runtime.mjs"), await readFile(new URL(import.meta.url)));
  await writeFile(join(controlDir, "safe-runtime-lib.mjs"), await readFile(new URL("./safe-runtime-lib.mjs", import.meta.url)));
  const node = process.execPath;
  await writeFile(join(safeHome, "start-safe-runtime.cmd"), `@echo off\r\n"${node}" "${join(controlDir, "safe-runtime.mjs")}" run\r\n`);
  await writeFile(join(safeHome, "apply-safe-runtime.cmd"), `@echo off\r\n"${node}" "${join(controlDir, "safe-runtime.mjs")}" apply --source "${sourceRoot}"\r\n`);
  console.log("Safe runtime installed. Stop the current dev servers once, then run:");
  console.log(join(safeHome, "start-safe-runtime.cmd"));
}

function attachBoundedLogs(child, name) {
  const logPath = join(logsDir, `${name}.log`);
  child.stdout?.on("data", (chunk) => {
    appendBoundedLog(logPath, chunk).catch(() => {});
  });
  child.stderr?.on("data", (chunk) => {
    appendBoundedLog(logPath, chunk).catch(() => {});
  });
}

function startChild(name, args, env) {
  const invocation = windowsCommand("npm.cmd", args);
  const child = spawn(invocation.command, invocation.args, { cwd: current, env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
  attachBoundedLogs(child, name);
  return child;
}

async function stopChild(child) {
  if (!child || child.exitCode !== null) return;
  try { await exec("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true }); } catch {}
  await waitForChildExit(child);
}

async function waitForChildExit(child, timeoutMs = 10000) {
  if (child.exitCode !== null) return;
  await new Promise((resolveExit, rejectExit) => {
    const timeout = setTimeout(() => rejectExit(new Error("Child process did not stop cleanly")), timeoutMs);
    child.once("exit", () => {
      clearTimeout(timeout);
      resolveExit();
    });
  });
}

async function healthy(url, timeoutMs = 180000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try { if ((await fetch(url)).ok) return true; } catch {}
    await delay(500);
  }
  return false;
}

async function supervise() {
  const settings = JSON.parse(await readFile(join(controlDir, "settings.json"), "utf8"));
  const canonicalSourceRoot = dirname(resolve(settings.configPath));
  const config = JSON.parse(await readFile(settings.configPath, "utf8"));
  const env = {
    ...process.env,
    ...pwaAllowedHostsEnvironment(config, process.env),
    CODE_REMOTE_MANAGER_CONFIG_PATH: settings.configPath
  };
  let backend;
  let pwa;
  let deploying = false;
  const healthUrls = runtimeHealthUrls(config, { pwaUrl: process.env.CODE_REMOTE_MANAGER_PWA_URL });
  const start = () => {
    backend = startChild("backend", ["run", "dev:backend"], env);
    pwa = startChild("pwa", ["run", "dev:pwa"], env);
  };
  start();
  for (;;) {
    let request;
    try { request = JSON.parse(await readFile(requestPath, "utf8")); } catch {}
    if (request && !deploying) {
      deploying = true;
      await rm(requestPath, { force: true });
      try {
        assertCanonicalSourceRoot(canonicalSourceRoot, request.sourceRoot);
        await verifySourceDiff(request.sourceRoot);
        await prepare(request.sourceRoot);
        await verify(staging);
        await stopChild(backend); await stopChild(pwa);
        await promoteRelease({ safeHome, current, staging, previous });
        start();
        const ok = (await Promise.all(healthUrls.map((url) => healthy(url)))).every(Boolean);
        if (!ok) {
          await stopChild(backend); await stopChild(pwa);
          await rollbackRelease({ safeHome, current, previous, failed });
          start();
          const recovered = (await Promise.all(healthUrls.map((url) => healthy(url)))).every(Boolean);
          if (!recovered) throw new Error("Previous release failed health checks after rollback");
          throw new Error("New release failed health checks; previous release restored");
        }
        await writeFile(resultPath, JSON.stringify({ id: request.id, ok: true }));
      } catch (error) {
        await rm(staging, { recursive: true, force: true });
        await writeFile(resultPath, JSON.stringify({ id: request.id, ok: false, message: redactOperationalError(error) }));
      }
      deploying = false;
    }
    if (!deploying && (backend?.exitCode !== null || pwa?.exitCode !== null)) {
      await stopChild(backend); await stopChild(pwa); await delay(1000); start();
    }
    await delay(500);
  }
}

async function apply() {
  const sourceRoot = resolve(argument("source", process.cwd()));
  const id = `${Date.now()}-${process.pid}`;
  const startedAt = Date.now();
  const settings = JSON.parse(await readFile(join(controlDir, "settings.json"), "utf8"));
  const canonicalSourceRoot = dirname(resolve(settings.configPath));
  assertCanonicalSourceRoot(canonicalSourceRoot, sourceRoot);
  await mkdir(controlDir, { recursive: true });
  const releaseApplyLock = await acquireApplyLock(applyLockPath, id);
  try {
    await rm(resultPath, { force: true });
    await writeFile(requestPath, JSON.stringify({ id, sourceRoot }));
    for (;;) {
      await delay(500);
      const requestPending = await readFile(requestPath).then(() => true, (error) => {
        if (error.code === "ENOENT") return false;
        throw error;
      });
      const waitError = safeApplyWaitError({ elapsedMs: Date.now() - startedAt, requestPending });
      if (waitError) throw new Error(waitError);
      try {
        const result = JSON.parse(await readFile(resultPath, "utf8"));
        if (result.id !== id) continue;
        if (!result.ok) throw new Error(result.message);
        console.log("Update applied.");
        return;
      } catch (error) {
        if (error.code === "ENOENT" || error instanceof SyntaxError) continue;
        throw error;
      }
    }
  } finally {
    try {
      const pendingRequest = JSON.parse(await readFile(requestPath, "utf8"));
      if (pendingRequest.id === id) await rm(requestPath, { force: true });
    } catch (error) {
      if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
    }
    await releaseApplyLock();
  }
}

const action = process.argv[2];
if (action === "install") await install();
else if (action === "run") await supervise();
else if (action === "apply") await apply();
else throw new Error("Use install, run, or apply");
