/**
 * world-watchdog-pid-lookup.mjs — host-visible PID lookup for the world watchdog.
 *
 * Uses `docker top <containerId>` to enumerate processes inside a world's
 * container and returns the host-visible PID of the claude process.
 *
 * `docker top` output format (Linux Docker / Colima):
 *   UID   PID   PPID  C  STIME  TTY  TIME     CMD
 *   root  1234  1     0  10:00  ?    00:00:00  node /usr/local/bin/claude ...
 *
 * The PID column (index 1 in default ps output) is already the host-visible
 * PID. On Mac/Colima the container runs inside a Linux VM so `docker top`
 * returns PIDs within the VM's PID namespace — these are NOT the macOS host
 * PIDs, but they ARE the PIDs visible from within the Linux layer (where
 * /proc reads happen). This is the same namespace the watchdog probes use
 * when reading /proc/<pid>/wchan etc., so the PIDs are correct for probe use.
 *
 * Inject `docker` for tests (avoids spawning real docker processes).
 *
 * @see docs/architecture/world-watchdog.md
 */

import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);

/**
 * Default docker executor — shells out to the real `docker` CLI.
 *
 * @param {string} containerId
 * @returns {Promise<string>}  stdout from `docker top <containerId>`
 */
async function defaultDockerTop(containerId) {
  const { stdout } = await execFileAsync('docker', ['top', containerId], {
    timeout: 5_000,
  });
  return stdout;
}

/**
 * Parse the stdout from `docker top` and extract host-visible PIDs whose
 * CMD column matches a claude process.
 *
 * docker top default output columns (ps -ef format):
 *   UID PID PPID C STIME TTY TIME CMD
 * Indices: 0=UID, 1=PID, 2=PPID, ..., 7+=CMD (rest of line after 7 columns).
 *
 * @param {string} stdout  Raw output from `docker top <id>`
 * @returns {number[]}     Host-visible PIDs of matching claude processes, sorted ascending.
 */
export function parseDockerTopOutput(stdout) {
  const lines = stdout.split('\n').filter((l) => l.trim().length > 0);
  if (lines.length < 2) return []; // header only or empty

  // Skip the header line (first line contains column names).
  const dataLines = lines.slice(1);

  const pids = [];
  for (const line of dataLines) {
    // Split on any whitespace — `docker top` columns are space-separated.
    // CMD may contain spaces; split into at most 8 parts (last = full CMD string).
    const parts = line.trim().split(/\s+/);
    if (parts.length < 8) continue;

    const pid = parseInt(parts[1], 10);
    if (!Number.isFinite(pid) || pid <= 0) continue;

    // parts[7] onward is the CMD. Rejoin the remainder.
    const cmd = parts.slice(7).join(' ');

    // Match: `claude` as standalone binary, or `node` process running claude.
    if (/(?:^|\/)claude(\s|$)/.test(cmd) || /node[^\s]*\s+.*[/\\]claude(?:\s|$)/.test(cmd)) {
      pids.push(pid);
    }
  }

  return pids.sort((a, b) => a - b);
}

/**
 * Find the host-visible PID of the claude process running inside a container.
 *
 * Returns the lowest matching PID (parent process heuristic — the supervisor
 * claude process has a lower PID than any child workers it spawns).
 *
 * Fail-soft:
 *   - docker unreachable / container not found → null + log
 *   - no claude process in the container → null (silent)
 *   - multiple claude processes → return the lowest PID
 *
 * @param {{
 *   containerId: string,
 *   dockerTop?: (containerId: string) => Promise<string>,
 *   log?: (msg: string) => void,
 * }} opts
 * @returns {Promise<number | null>}
 */
export async function findClaudePid({
  containerId,
  dockerTop = defaultDockerTop,
  log = (m) => console.log(`[world-watchdog-pid-lookup] ${m}`),
}) {
  if (!containerId) return null;

  let stdout;
  try {
    stdout = await dockerTop(containerId);
  } catch (err) {
    log(`docker top ${containerId} failed: ${err?.message ?? err}`);
    return null;
  }

  const pids = parseDockerTopOutput(stdout);
  if (pids.length === 0) return null;

  // Lowest PID = the parent/supervisor process.
  return pids[0];
}
