#!/usr/bin/env node

import { spawn } from "node:child_process";
import { existsSync, readFileSync, statSync } from "node:fs";
import { access } from "node:fs/promises";
import module from "node:module";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

const MIN_NODE_MAJOR = 22;
const MIN_NODE_MINOR = 19;
const MIN_NODE_VERSION = `${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}`;

const parseNodeVersion = (rawVersion) => {
  const [majorRaw = "0", minorRaw = "0"] = rawVersion.split(".");
  return {
    major: Number(majorRaw),
    minor: Number(minorRaw),
  };
};

const isSupportedNodeVersion = (version) =>
  version.major > MIN_NODE_MAJOR ||
  (version.major === MIN_NODE_MAJOR && version.minor >= MIN_NODE_MINOR);

const ensureSupportedNodeVersion = () => {
  if (isSupportedNodeVersion(parseNodeVersion(process.versions.node))) {
    return;
  }

  process.stderr.write(
    `daocore: Node.js v${MIN_NODE_VERSION}+ is required (current: v${process.versions.node}).\n` +
      "If you use nvm, run:\n" +
      `  nvm install ${MIN_NODE_MAJOR}\n` +
      `  nvm use ${MIN_NODE_MAJOR}\n` +
      `  nvm alias default ${MIN_NODE_MAJOR}\n`,
  );
  process.exit(1);
};

ensureSupportedNodeVersion();

const isSourceCheckoutLauncher = () =>
  existsSync(new URL("./.git", import.meta.url)) ||
  existsSync(new URL("./src/entry.ts", import.meta.url));

const isNodeCompileCacheDisabled = () => process.env.NODE_DISABLE_COMPILE_CACHE !== undefined;
const isNodeCompileCacheRequested = () =>
  Boolean(process.env.NODE_COMPILE_CACHE) && !isNodeCompileCacheDisabled();
const sanitizeCompileCachePathSegment = (value) => {
  const normalized = value.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
  return normalized.length > 0 ? normalized : "unknown";
};
const readPackageVersion = () => {
  try {
    const parsed = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8"));
    if (typeof parsed?.version === "string" && parsed.version.trim().length > 0) {
      return parsed.version;
    }
  } catch {
    // Fall through to an install-metadata-only cache key.
  }
  return "unknown";
};
const resolvePackagedCompileCacheDirectory = () => {
  const packageJsonUrl = new URL("./package.json", import.meta.url);
  const version = sanitizeCompileCachePathSegment(readPackageVersion());
  let installMarker = "no-package-json";
  try {
    const stat = statSync(packageJsonUrl);
    installMarker = `${Math.trunc(stat.mtimeMs)}-${stat.size}`;
  } catch {
    // Package archives should always have package.json, but keep startup best-effort.
  }
  const baseDirectory = isNodeCompileCacheRequested()
    ? process.env.NODE_COMPILE_CACHE
    : path.join(os.tmpdir(), "node-compile-cache");
  return path.join(
    baseDirectory,
    "daocore",
    version,
    sanitizeCompileCachePathSegment(installMarker),
  );
};

const respawnSignals =
  process.platform === "win32"
    ? ["SIGTERM", "SIGINT", "SIGBREAK"]
    : ["SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT"];
const respawnSignalExitGraceMs = 1_000;
const respawnSignalForceKillGraceMs = 1_000;

const runRespawnedChild = (command, args, env) => {
  const child = spawn(command, args, {
    stdio: "inherit",
    env,
  });
  const listeners = new Map();
  // This intentionally overlaps with src/entry.compile-cache.ts; keep the
  // respawn supervision behavior in sync until the launcher can share TS code.
  // Give the child a moment to honor forwarded signals, then exit the wrapper so
  // a child that ignores SIGTERM cannot keep the launcher alive indefinitely.
  let signalExitTimer = null;
  let signalForceKillTimer = null;
  const detach = () => {
    for (const [signal, listener] of listeners) {
      process.off(signal, listener);
    }
    listeners.clear();
    if (signalExitTimer) {
      clearTimeout(signalExitTimer);
      signalExitTimer = null;
    }
    if (signalForceKillTimer) {
      clearTimeout(signalForceKillTimer);
      signalForceKillTimer = null;
    }
  };
  const forceKillChild = () => {
    try {
      child.kill(process.platform === "win32" ? "SIGTERM" : "SIGKILL");
    } catch {
      // Best-effort shutdown fallback.
    }
  };
  const requestChildTermination = () => {
    try {
      child.kill("SIGTERM");
    } catch {
      // Best-effort shutdown fallback.
    }
    signalForceKillTimer = setTimeout(() => {
      forceKillChild();
      process.exit(1);
    }, respawnSignalForceKillGraceMs);
    signalForceKillTimer.unref?.();
  };
  const scheduleParentExit = () => {
    if (signalExitTimer) {
      return;
    }
    signalExitTimer = setTimeout(() => {
      requestChildTermination();
    }, respawnSignalExitGraceMs);
    signalExitTimer.unref?.();
  };
  for (const signal of respawnSignals) {
    const listener = () => {
      try {
        child.kill(signal);
      } catch {
        // Best-effort signal forwarding.
      }
      scheduleParentExit();
    };
    try {
      process.on(signal, listener);
      listeners.set(signal, listener);
    } catch {
      // Unsupported signal on this platform.
    }
  }
  child.once("exit", (code, signal) => {
    detach();
    if (signal) {
      process.exit(1);
    }
    process.exit(code ?? 1);
  });
  child.once("error", (error) => {
    detach();
    process.stderr.write(
      `[daocore] Failed to respawn launcher: ${
        error instanceof Error ? (error.stack ?? error.message) : String(error)
      }\n`,
    );
    process.exit(1);
  });
  return true;
};

const respawnWithoutCompileCacheIfNeeded = () => {
  if (!isSourceCheckoutLauncher()) {
    return false;
  }
  if (process.env.DAOCORE_SOURCE_COMPILE_CACHE_RESPAWNED === "1") {
    return false;
  }
  if (!module.getCompileCacheDir?.() && !isNodeCompileCacheRequested()) {
    return false;
  }
  const env = {
    ...process.env,
    NODE_DISABLE_COMPILE_CACHE: "1",
    DAOCORE_SOURCE_COMPILE_CACHE_RESPAWNED: "1",
  };
  delete env.NODE_COMPILE_CACHE;
  return runRespawnedChild(
    process.execPath,
    [...process.execArgv, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
    env,
  );
};

const respawnWithPackagedCompileCacheIfNeeded = () => {
  if (isSourceCheckoutLauncher() || isNodeCompileCacheDisabled()) {
    return false;
  }
  if (process.env.DAOCORE_PACKAGED_COMPILE_CACHE_RESPAWNED === "1") {
    return false;
  }
  const currentDirectory = module.getCompileCacheDir?.();
  if (!currentDirectory) {
    return false;
  }
  const desiredDirectory = resolvePackagedCompileCacheDirectory();
  if (path.resolve(currentDirectory) === path.resolve(desiredDirectory)) {
    return false;
  }
  const env = {
    ...process.env,
    NODE_COMPILE_CACHE: desiredDirectory,
    DAOCORE_PACKAGED_COMPILE_CACHE_RESPAWNED: "1",
  };
  return runRespawnedChild(
    process.execPath,
    [...process.execArgv, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
    env,
  );
};

const waitingForCompileCacheRespawn =
  respawnWithoutCompileCacheIfNeeded() || respawnWithPackagedCompileCacheIfNeeded();

// https://nodejs.org/api/module.html#module-compile-cache
if (
  !waitingForCompileCacheRespawn &&
  module.enableCompileCache &&
  !isNodeCompileCacheDisabled() &&
  !isSourceCheckoutLauncher()
) {
  try {
    module.enableCompileCache(resolvePackagedCompileCacheDirectory());
  } catch {
    // Ignore errors
  }
}

const isModuleNotFoundError = (err) =>
  err && typeof err === "object" && "code" in err && err.code === "ERR_MODULE_NOT_FOUND";

const isDirectModuleNotFoundError = (err, specifier) => {
  if (!isModuleNotFoundError(err)) {
    return false;
  }

  const expectedUrl = new URL(specifier, import.meta.url);
  if ("url" in err && err.url === expectedUrl.href) {
    return true;
  }

  const message = "message" in err && typeof err.message === "string" ? err.message : "";
  const expectedPath = fileURLToPath(expectedUrl);
  return (
    message.includes(`Cannot find module '${expectedPath}'`) ||
    message.includes(`Cannot find module "${expectedPath}"`)
  );
};

const installProcessWarningFilter = async () => {
  // Keep bootstrap warnings consistent with the TypeScript runtime.
  for (const specifier of ["./dist/warning-filter.js", "./dist/warning-filter.mjs"]) {
    try {
      const mod = await import(specifier);
      if (typeof mod.installProcessWarningFilter === "function") {
        mod.installProcessWarningFilter();
        return;
      }
    } catch (err) {
      if (isDirectModuleNotFoundError(err, specifier)) {
        continue;
      }
      throw err;
    }
  }
};

const tryImport = async (specifier) => {
  try {
    await import(specifier);
    return true;
  } catch (err) {
    // Only swallow direct entry misses; rethrow transitive resolution failures.
    if (isDirectModuleNotFoundError(err, specifier)) {
      return false;
    }
    throw err;
  }
};

const exists = async (specifier) => {
  try {
    await access(new URL(specifier, import.meta.url));
    return true;
  } catch {
    return false;
  }
};

const buildMissingEntryErrorMessage = async () => {
  const lines = ["daocore: missing dist/entry.(m)js (build output)."];
  if (!(await exists("./src/entry.ts"))) {
    return lines.join("\n");
  }

  lines.push("This install looks like an unbuilt source tree or GitHub source archive.");
  lines.push(
    "Build locally with `pnpm install && pnpm build`, or install a built package instead.",
  );
  lines.push(
    "For pinned GitHub installs, use `npm install -g github:daocore/daocore#<ref>` instead of a raw `/archive/<ref>.tar.gz` URL.",
  );
  lines.push("For releases, use `npm install -g daocore@latest`.");
  return lines.join("\n");
};

const isBareRootHelpInvocation = (argv) =>
  argv.length === 3 && (argv[2] === "--help" || argv[2] === "-h");

const resolvePrecomputedCommandHelp = (argv) => {
  if (argv.length !== 4 || (argv[3] !== "--help" && argv[3] !== "-h")) {
    return null;
  }
  if (argv[2] === "browser") {
    return { command: "browser", metadataKey: "browserHelpText" };
  }
  if (argv[2] === "secrets") {
    return { command: "secrets", metadataKey: "secretsHelpText" };
  }
  if (argv[2] === "nodes") {
    return { command: "nodes", metadataKey: "nodesHelpText" };
  }
  return null;
};

const isHelpFastPathDisabled = () => process.env.DAOCORE_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1";

const normalizeLauncherHomeValue = (value) => {
  const trimmed = value?.trim();
  return trimmed && trimmed !== "undefined" && trimmed !== "null" ? trimmed : undefined;
};

const resolveLauncherOsHomeDir = () =>
  normalizeLauncherHomeValue(process.env.HOME) ??
  normalizeLauncherHomeValue(process.env.USERPROFILE) ??
  os.homedir();

const resolveLauncherHomeDir = () => {
  const explicit = normalizeLauncherHomeValue(process.env.DAOCORE_HOME);
  const rawHome =
    explicit && (explicit === "~" || explicit.startsWith("~/") || explicit.startsWith("~\\"))
      ? explicit.replace(/^~(?=$|[\\/])/, resolveLauncherOsHomeDir())
      : (explicit ?? resolveLauncherOsHomeDir());
  return path.resolve(rawHome);
};

const resolveLauncherUserPath = (input) => {
  if (input === "~") {
    return resolveLauncherHomeDir();
  }
  if (input.startsWith("~/") || input.startsWith("~\\")) {
    return path.join(resolveLauncherHomeDir(), input.slice(2));
  }
  return path.resolve(input);
};

const resolveLauncherConfigPaths = () => {
  const explicit = process.env.DAOCORE_CONFIG_PATH?.trim();
  if (explicit) {
    return [resolveLauncherUserPath(explicit)];
  }
  const stateOverride = process.env.DAOCORE_STATE_DIR?.trim();
  if (stateOverride) {
    const stateDir = resolveLauncherUserPath(stateOverride);
    return [path.join(stateDir, "daocore.json"), path.join(stateDir, "clawdbot.json")];
  }
  const homeDir = resolveLauncherHomeDir();
  return [
    path.join(homeDir, ".daocore", "daocore.json"),
    path.join(homeDir, ".daocore", "clawdbot.json"),
    path.join(homeDir, ".clawdbot", "daocore.json"),
    path.join(homeDir, ".clawdbot", "clawdbot.json"),
  ];
};

const shouldDeferRootHelpToRuntimeEntry = () => {
  if (
    process.env.DAOCORE_BUNDLED_PLUGINS_DIR?.trim() ||
    process.env.DAOCORE_DISABLE_BUNDLED_PLUGINS?.trim()
  ) {
    return true;
  }
  for (const configPath of resolveLauncherConfigPaths()) {
    try {
      const raw = readFileSync(configPath, "utf8");
      return /\bplugins\b|\$include\b/.test(raw);
    } catch {
      continue;
    }
  }
  return false;
};

const loadPrecomputedHelpText = (key) => {
  try {
    const raw = readFileSync(new URL("./dist/cli-startup-metadata.json", import.meta.url), "utf8");
    const parsed = JSON.parse(raw);
    const value = parsed?.[key];
    return typeof value === "string" && value.length > 0 ? value : null;
  } catch {
    return null;
  }
};

const tryOutputBareRootHelp = async () => {
  if (!isBareRootHelpInvocation(process.argv)) {
    return false;
  }
  if (shouldDeferRootHelpToRuntimeEntry()) {
    return false;
  }
  const precomputed = loadPrecomputedHelpText("rootHelpText");
  if (precomputed) {
    process.stdout.write(precomputed);
    return true;
  }
  for (const specifier of ["./dist/cli/program/root-help.js", "./dist/cli/program/root-help.mjs"]) {
    try {
      const mod = await import(specifier);
      if (typeof mod.outputRootHelp === "function") {
        await mod.outputRootHelp();
        return true;
      }
    } catch (err) {
      if (isDirectModuleNotFoundError(err, specifier)) {
        continue;
      }
      throw err;
    }
  }
  return false;
};

const tryOutputPrecomputedCommandHelp = () => {
  const commandHelp = resolvePrecomputedCommandHelp(process.argv);
  if (!commandHelp) {
    return false;
  }
  if (commandHelp.command === "nodes" && shouldDeferRootHelpToRuntimeEntry()) {
    return false;
  }
  const precomputed = loadPrecomputedHelpText(commandHelp.metadataKey);
  if (!precomputed) {
    return false;
  }
  process.stdout.write(precomputed);
  return true;
};

if (!waitingForCompileCacheRespawn) {
  if (!isHelpFastPathDisabled() && (await tryOutputBareRootHelp())) {
    // OK
  } else if (!isHelpFastPathDisabled() && tryOutputPrecomputedCommandHelp()) {
    // OK
  } else {
    await installProcessWarningFilter();
    if (await tryImport("./dist/entry.js")) {
      // OK
    } else if (await tryImport("./dist/entry.mjs")) {
      // OK
    } else {
      throw new Error(await buildMissingEntryErrorMessage());
    }
  }
}
