#!/usr/bin/env node
"use strict";

/**
 * Full dev mode — no packing, no global install needed.
 *
 * What it does:
 *   1. Builds native devtools dylibs (libArgentInjectionBootstrap, libNativeDevtoolsIos, libKeyboardPatch)
 *   2. Builds the dispatcher TypeScript (tsc only, no esbuild bundles)
 *   3. Sets up packages/argent/bin/ and packages/argent/dist/ for local use
 *   4. Patches supported editor MCP configs to point argent at the local dist
 *   5. Starts the tool-server from source via ts-node (no build needed)
 *   6. Writes ~/.argent/tool-server.json so the local MCP picks it up
 *   7. On exit: restores patched editor configs and stops the tool-server
 *
 * Usage:
 *   npm run dev
 *   PORT=4000 npm run dev
 */

const { execSync, spawn } = require("child_process");
const fs = require("fs");
const path = require("path");
const os = require("os");

const ROOT = path.resolve(__dirname, "..");
const ARGENT_PKG = path.join(ROOT, "packages", "argent");
const TOOL_SERVER_PKG = path.join(ROOT, "packages", "tool-server");
const NATIVE_DEVTOOLS_PKG = path.join(ROOT, "packages", "native-devtools-ios");
const STATE_DIR = path.join(os.homedir(), ".argent");
const STATE_FILE = path.join(STATE_DIR, "tool-server.json");
const CLAUDE_JSON = path.join(os.homedir(), ".claude.json");
const CURSOR_DIR = path.join(os.homedir(), ".cursor");
const CURSOR_MCP_JSON = path.join(CURSOR_DIR, "mcp.json");
const PORT = parseInt(process.env.PORT ?? "3001", 10);

// ── Helpers ───────────────────────────────────────────────────────────────────

function readJson(filePath) {
  if (!fs.existsSync(filePath)) return {};
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
}

function writeJson(filePath, data) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
}

function isProcessAlive(pid) {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

async function waitForHttp(url, timeoutMs = 20_000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try {
      const res = await fetch(url, { signal: AbortSignal.timeout(1000) });
      if (res.ok) return true;
    } catch {
      /* not up yet — keep polling until the deadline */
    }
    await new Promise((r) => setTimeout(r, 400));
  }
  return false;
}

// ── Step 1: Build native devtools dylibs (macOS only) ───────────────────────

// Dylibs are macOS-only at runtime (DYLD_INSERT_LIBRARIES into iOS Simulator)
// and macOS-only at build time (Xcode). Skip on Linux/Windows so a contributor
// without `argent-private` SSH access can still run `npm run dev`.
if (process.platform === "darwin") {
  const DYLIBS_DIR = path.join(NATIVE_DEVTOOLS_PKG, "dylibs");
  const DYLIBS_EXIST = fs.existsSync(path.join(DYLIBS_DIR, "libNativeDevtoolsIos.dylib"));
  const PRIVATE_NATIVE_DEVTOOLS_SRC = path.join(
    ROOT,
    "packages",
    "argent-private",
    "packages",
    "native-devtools-ios",
    "Sources",
    "NativeDevtoolsIos"
  );

  // Try to init the submodule and rebuild. Failure is non-fatal if pre-built
  // dylibs are already present — developers without argent-private access can
  // still work on Argent using the committed binaries.
  let submoduleReady = false;
  try {
    // Preserve an existing argent-private checkout so local branch switches
    // are not reset back to the superproject's recorded gitlink on every dev run.
    if (!fs.existsSync(PRIVATE_NATIVE_DEVTOOLS_SRC)) {
      execSync("git submodule update --init packages/argent-private", {
        cwd: ROOT,
        stdio: "pipe",
      });
    }
    submoduleReady = true;
  } catch {
    if (DYLIBS_EXIST) {
      console.warn("⚠ argent-private submodule unavailable — using pre-built dylibs\n");
    } else {
      console.error("✗ argent-private submodule unavailable and no pre-built dylibs found.");
      console.error("  Grant SSH access to github.com/software-mansion-labs/argent-private");
      console.error(
        "  or obtain pre-built dylibs and place them in packages/native-devtools-ios/dylibs/"
      );
      process.exit(1);
    }
  }

  if (submoduleReady) {
    console.log("Building native devtools dylibs...");
    execSync("bash scripts/build.sh dev", {
      cwd: NATIVE_DEVTOOLS_PKG,
      stdio: "inherit",
    });
    console.log("✓ Native devtools dylibs built\n");
  }
} else {
  console.log(`⊘ Skipping native devtools dylibs build (macOS-only on ${process.platform})\n`);
}

// ── Step 2: Build MCP TypeScript ─────────────────────────────────────────────

console.log("Building dispatcher TypeScript...");
execSync("npm run build:dispatcher -w @swmansion/argent", {
  cwd: ROOT,
  stdio: "inherit",
});
console.log("✓ Dispatcher TypeScript built\n");

// ── Step 3: Set up packages/argent/bin/ and skills/rules/agents ──────────────

// Copy the simulator-server binary for the current host platform into the
// platform-keyed subdir the runtime resolver looks at. Mirrors bundle-tools.cjs
// but only for the current host's key — dev iterations don't need every
// host's binary alongside. The key mirrors hostPlatformKey() in
// @argent/native-devtools-ios (process.platform, except "linux-arm64" on
// arm64 Linux).
const HOST_PLATFORM_KEY =
  process.platform === "linux" && process.arch === "arm64" ? "linux-arm64" : process.platform;
const BIN_DIR = path.join(ARGENT_PKG, "bin", HOST_PLATFORM_KEY);
// win32 ships a PE `.exe` (simulator-server.exe); every other host an
// extensionless binary. Mirrors simulatorServerBinaryName() in
// @argent/native-devtools-ios and bundle-tools.cjs.
const BIN_BASENAME = process.platform === "win32" ? "simulator-server.exe" : "simulator-server";
const BIN_SRC = path.join(NATIVE_DEVTOOLS_PKG, "bin", HOST_PLATFORM_KEY, BIN_BASENAME);
const BIN_DEST = path.join(BIN_DIR, BIN_BASENAME);
fs.mkdirSync(BIN_DIR, { recursive: true });
if (fs.existsSync(BIN_SRC)) {
  fs.copyFileSync(BIN_SRC, BIN_DEST);
  fs.chmodSync(BIN_DEST, 0o755);
  console.log(`✓ Copied simulator-server binary (${HOST_PLATFORM_KEY})`);
} else {
  console.warn(
    `⚠ simulator-server binary not found at ${BIN_SRC} — gestures won't work. ` +
      `Run: bash scripts/download-simulator-server.sh`
  );
}

for (const [srcName, destName] of [
  ["skills/skills", "skills"],
  ["skills/rules", "rules"],
  ["skills/agents", "agents"],
]) {
  const src = path.join(ROOT, "packages", srcName);
  const dest = path.join(ARGENT_PKG, destName);
  if (fs.existsSync(src)) {
    fs.rmSync(dest, { recursive: true, force: true });
    fs.cpSync(src, dest, { recursive: true });
  }
}
console.log("✓ Copied skills/rules/agents");

// Stub tool-server.cjs — the launcher won't use it as long as the dev server
// is registered in the state file, but it needs to exist to avoid a crash.
const STUB = path.join(ARGENT_PKG, "dist", "tool-server.cjs");
if (!fs.existsSync(STUB)) {
  fs.mkdirSync(path.dirname(STUB), { recursive: true });
  fs.writeFileSync(
    STUB,
    "throw new Error('dev mode: tool-server stub — start npm run dev first');\n"
  );
}

function restoreMcpEntry(configPath, originalEntry, existedBefore) {
  const config = readJson(configPath);
  if (!config.mcpServers) config.mcpServers = {};

  if (originalEntry) {
    config.mcpServers.argent = originalEntry;
  } else {
    delete config.mcpServers.argent;
  }

  if (config.mcpServers && Object.keys(config.mcpServers).length === 0) {
    delete config.mcpServers;
  }

  if (!existedBefore && Object.keys(config).length === 0) {
    try {
      fs.unlinkSync(configPath);
    } catch {
      /* file already absent — nothing to remove */
    }
    return;
  }

  writeJson(configPath, config);
}

// ── Step 4: Patch editor MCP configs to use local MCP dist ───────────────────

const LOCAL_MCP_ENTRY = path.join(ARGENT_PKG, "dist", "cli.js");
const LOG_FILE = path.join(STATE_DIR, "mcp-calls.log");

const claudeConfigExists = fs.existsSync(CLAUDE_JSON);
const claudeConfig = readJson(CLAUDE_JSON);
const originalArgentEntry = claudeConfig?.mcpServers?.argent ?? null;
const shouldPatchCursor = fs.existsSync(CURSOR_DIR);
const cursorConfigExists = fs.existsSync(CURSOR_MCP_JSON);
const cursorConfig = shouldPatchCursor ? readJson(CURSOR_MCP_JSON) : {};
const originalCursorArgentEntry = cursorConfig?.mcpServers?.argent ?? null;

const devMcpEntry = {
  type: "stdio",
  command: "node",
  args: [LOCAL_MCP_ENTRY, "mcp"],
  // Point the dev MCP straight at the running dev tool-server. Without this
  // it calls ensureToolsServer(), which now treats the tokenless dev state
  // as stale, SIGTERMs the dev server and tries to spawn the throwing dev
  // stub — breaking `npm run dev`. The dev server runs auth-disabled, so no
  // ARGENT_AUTH_TOKEN is needed here.
  env: { ARGENT_MCP_LOG: LOG_FILE, ARGENT_TOOLS_URL: `http://127.0.0.1:${PORT}` },
};

if (!claudeConfig.mcpServers) claudeConfig.mcpServers = {};
claudeConfig.mcpServers.argent = devMcpEntry;
writeJson(CLAUDE_JSON, claudeConfig);
console.log(`✓ Patched ~/.claude.json → node ${LOCAL_MCP_ENTRY} mcp`);

if (shouldPatchCursor) {
  if (!cursorConfig.mcpServers) cursorConfig.mcpServers = {};
  cursorConfig.mcpServers.argent = {
    command: "node",
    args: [LOCAL_MCP_ENTRY, "mcp"],
    // Same env as the Claude entry (incl. ARGENT_TOOLS_URL) so Cursor's dev
    // MCP also reuses the running dev tool-server instead of killing it.
    env: devMcpEntry.env,
  };
  writeJson(CURSOR_MCP_JSON, cursorConfig);
  console.log(`✓ Patched ~/.cursor/mcp.json → node ${LOCAL_MCP_ENTRY} mcp\n`);
} else {
  console.log("• Skipped Cursor patch (no ~/.cursor directory found)\n");
}

// ── Cleanup on exit ───────────────────────────────────────────────────────────

let toolServerPid = null;

function cleanup() {
  console.log("\nCleaning up...");

  // Stop tool-server
  if (toolServerPid && isProcessAlive(toolServerPid)) {
    process.kill(toolServerPid, "SIGTERM");
  }
  try {
    fs.unlinkSync(STATE_FILE);
  } catch {
    /* state file already absent — nothing to remove */
  }

  // Restore editor configs
  restoreMcpEntry(CLAUDE_JSON, originalArgentEntry, claudeConfigExists);
  console.log("✓ Restored ~/.claude.json");
  if (shouldPatchCursor) {
    restoreMcpEntry(CURSOR_MCP_JSON, originalCursorArgentEntry, cursorConfigExists);
    console.log("✓ Restored ~/.cursor/mcp.json");
  }
  console.log("Done.");
}

process.on("SIGINT", () => {
  cleanup();
  process.exit(0);
});
process.on("SIGTERM", () => {
  cleanup();
  process.exit(0);
});
process.on("exit", cleanup);

async function main() {
  // ── Step 5: Kill any existing registered tool-server ───────────────────────

  fs.mkdirSync(STATE_DIR, { recursive: true });
  const existingState = readJson(STATE_FILE);
  if (existingState.pid && isProcessAlive(existingState.pid)) {
    console.log(`Stopping existing tool-server (PID ${existingState.pid})...`);
    process.kill(existingState.pid, "SIGTERM");
    await new Promise((r) => setTimeout(r, 600));
  }
  try {
    fs.unlinkSync(STATE_FILE);
  } catch {
    /* state file already absent — nothing to remove */
  }

  // ── Step 6: Start tool-server from source ──────────────────────────────────

  console.log(`Starting dev tool-server on port ${PORT}...`);

  const toolServer = spawn("npx", ["ts-node", "src/index.ts"], {
    cwd: TOOL_SERVER_PKG,
    stdio: ["ignore", "pipe", "pipe"],
    env: { ...process.env, PORT: String(PORT) },
  });

  toolServerPid = toolServer.pid;

  const logStream = fs.createWriteStream(path.join(STATE_DIR, "tool-server.log"), { flags: "a" });
  toolServer.stdout.pipe(logStream);
  toolServer.stderr.pipe(logStream);

  toolServer.on("exit", (code) => {
    if (code !== 0 && code !== null) {
      console.error(`\nTool-server exited with code ${code}. Check ~/.argent/tool-server.log`);
    }
  });

  // ── Step 7: Wait for tool-server to be ready ───────────────────────────────

  process.stdout.write("Waiting for tool-server");
  const ready = await waitForHttp(`http://127.0.0.1:${PORT}/tools`);
  if (!ready) {
    console.error("\nTool-server failed to start. Check ~/.argent/tool-server.log");
    cleanup();
    process.exit(1);
  }
  console.log(" ready.");

  // ── Step 8: Write state file ────────────────────────────────────────────────

  writeJson(STATE_FILE, {
    port: PORT,
    pid: toolServerPid,
    startedAt: new Date().toISOString(),
    bundlePath: "dev",
  });

  // ── Done ────────────────────────────────────────────────────────────────────

  console.log(`
✓ Dev environment ready
  Tool-server: http://127.0.0.1:${PORT}/tools
  MCP:         ${LOCAL_MCP_ENTRY}
  Logs:        ~/.argent/tool-server.log

  Start a new Claude Code or Cursor session to pick up the local MCP.

  After tool-server code changes → Ctrl+C and re-run npm run dev
  After MCP code changes         → re-run npm run dev (rebuilds MCP automatically)

Press Ctrl+C to stop and restore global argent.
`);

  // Keep alive until tool-server exits or Ctrl+C
  await new Promise((resolve) => {
    toolServer.on("exit", resolve);
  });
}

main().catch((err) => {
  console.error(err);
  cleanup();
  process.exit(1);
});
