#!/usr/bin/env node
/**
 * dev-guard — catches the #1 cause of memory exhaustion before a second
 *             dev server boots and lifts total system RSS unnecessarily.
 *
 * Detects Vite parents under the same `pnpm dev` lineage. A single Vite
 * dev server stabilizes at ~250–500 MB; stacking two doubles total RSS,
 * confuses HMR (two ports, two websockets), and creates "which one am I
 * actually viewing?" mistakes when designers compare branches.
 *
 * (History: until PR-6 (May 2026) this guard also detected `next-server`
 * parents because `apps/web` ran on Next + Turbopack. After Next was
 * removed, the Next branch became dead code — this version is Vite-only.)
 *
 * Run as `predev` hook (or any time you want to verify environment health):
 *   "predev": "exxat-ui dev-guard"
 *
 * Behavior:
 *   - Lists every running `vite` parent.
 *   - Exits 0 silently if zero or one is found (the one is *us* — fine).
 *   - Prints a guided warning if 2+ are found, names the cwd of each,
 *     and asks the user whether to abort or continue.
 *   - In CI (`CI=true`) or with `--quiet`, exits 0 without prompting.
 *
 * Tested against macOS / Linux. Windows users: PowerShell `Get-Process` would
 * need a separate branch; not implemented yet because the team is mac/linux.
 */

import { execFileSync } from "node:child_process"
import { createInterface } from "node:readline/promises"

const QUIET = process.argv.includes("--quiet") || process.env.CI === "true"

/** @typedef {{ pid: number, cwd: string, rss: number }} ViteProc */

/** @returns {ViteProc[]} */
function listDevServers() {
  let out = ""
  try {
    out = execFileSync(
      "ps",
      ["-axo", "pid=,rss=,command="],
      { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] },
    )
  } catch {
    // ps unavailable (very unusual). Fail open — better to let dev start.
    return []
  }

  const matches = []
  for (const line of out.split("\n")) {
    const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.+)$/)
    if (!m) continue
    const [, pid, rss, command] = m

    // Vite — match the executable's path component. The Vite process is
    // always spawned as `node .../vite/bin/vite[.js|.mjs]` or via the
    // binstub `node .../node_modules/.bin/vite`. The `/vite` token MUST be
    // path-anchored (preceded by `/`) so we don't false-positive on:
    //   • shell wrappers: `/bin/zsh -c "... pnpm exec vite ..."`
    //   • pnpm exec wrapper: `node /opt/homebrew/bin/pnpm exec vite ...`
    //     (these match `vite` as a literal argv but no Vite is running yet)
    //   • vitest, vite preview, esbuild children.
    //
    // Caught in 0.6.7 verification: dev-guard was flagging the shell + pnpm
    // wrapper around a freshly-launched `pnpm dev`, surfacing a phantom
    // "multiple dev servers" warning on every first-run.
    const isViteDev =
      /\/vite(?:\.[mj]s)?(?:\s|$)/.test(command) &&
      !/\bvitest\b/.test(command) &&
      !/\bvite\s+preview\b/.test(command) &&
      !/\besbuild\b/.test(command) &&
      !/\bpnpm\s+exec\s+vite\b/.test(command)

    if (!isViteDev) continue
    matches.push({
      pid: Number(pid),
      cwd: cwdOfPid(Number(pid)) ?? "(cwd unknown)",
      rss: Number(rss), // KB
    })
  }
  return matches
}

/** Best-effort cwd lookup. macOS has lsof; Linux has /proc/<pid>/cwd. */
function cwdOfPid(pid) {
  try {
    if (process.platform === "darwin") {
      const out = execFileSync("lsof", ["-p", String(pid), "-d", "cwd", "-Fn"], {
        encoding: "utf8",
        stdio: ["ignore", "pipe", "ignore"],
      })
      const line = out.split("\n").find(l => l.startsWith("n"))
      return line ? line.slice(1) : null
    }
    if (process.platform === "linux") {
      const out = execFileSync("readlink", ["-f", `/proc/${pid}/cwd`], {
        encoding: "utf8",
        stdio: ["ignore", "pipe", "ignore"],
      })
      return out.trim() || null
    }
  } catch {
    return null
  }
  return null
}

function fmtMB(kb) {
  return (kb / 1024).toFixed(0) + " MB"
}

async function main() {
  const servers = listDevServers()
  if (servers.length <= 1) {
    if (!QUIET) {
      console.log(
        servers.length === 0
          ? "[dev-guard] No other dev server running. Safe to start."
          : `[dev-guard] One vite already running (pid ${servers[0].pid}, ${fmtMB(servers[0].rss)}). Reusing it.`,
      )
    }
    process.exit(0)
  }

  // Two or more — this is the bad path.
  console.error("")
  console.error("⚠  dev-guard: multiple Vite dev-server processes detected.")
  console.error("")
  console.error("Two parallel Vite dev servers stabilize at ~500–800 MB combined")
  console.error("and confuse HMR (two ports, two websockets, components from two")
  console.error("workspace roots). Stop the one you're not actively touching.")
  console.error("")
  for (const s of servers) {
    console.error(
      `  pid ${String(s.pid).padEnd(6)}  ${fmtMB(s.rss).padEnd(8)}  ${s.cwd}`,
    )
  }
  console.error("")
  console.error("Recommended: stop the one you're not actively touching.")
  console.error("  • macOS / Linux: kill <pid>")
  console.error("  • pm2 daemon:    pm2 stop exxat-ds (or pm2 delete exxat-ds)")
  console.error("")

  if (QUIET) {
    // CI / quiet mode — don't block, just warn.
    process.exit(0)
  }

  const rl = createInterface({ input: process.stdin, output: process.stdout })
  const ans = (await rl.question("Continue starting another dev server anyway? [y/N] ")).trim().toLowerCase()
  rl.close()

  if (ans === "y" || ans === "yes") {
    console.error("[dev-guard] Continuing.")
    process.exit(0)
  }
  console.error("[dev-guard] Aborted. Stop the other server, then re-run dev.")
  process.exit(1)
}

main().catch(err => {
  // Never let a guard bug block dev. Log and pass through.
  console.error("[dev-guard] internal error, allowing dev to start:", err?.message ?? err)
  process.exit(0)
})
