'use strict';
/**
 * mcp-stdio-runtime — the survival kit every AgentsRoom MCP stdio server loads
 * before anything else.
 *
 * Why this file exists
 * --------------------
 * An MCP stdio server is a plain Node subprocess spawned by the agent CLI. The
 * CLI has exactly one way to notice a problem: the process exiting. When that
 * happens mid-session the user gets "MCP server disconnected" / "MCP client
 * failed to start", every AgentsRoom tool vanishes from that agent, and nothing
 * respawns it — they have to restart the agent and lose their context. So the
 * bar for these processes is not "handle errors nicely", it is NEVER DIE.
 *
 * Three ways they used to die, all fixed here:
 *
 *   1. An unhandled promise rejection. Node >= 15 makes that fatal by default,
 *      and none of the five servers had a handler. One stray rejection inside a
 *      tool call — a socket that errors after its promise settled, a `JSON.parse`
 *      in a callback — took the whole server down.
 *   2. An `EPIPE` (or `ENOSPC`) on stdout. `process.stdout` emits `'error'`;
 *      with no listener that is an uncaught exception. It happens naturally
 *      when the CLI goes away first, turning an orderly shutdown into a crash.
 *   3. A synchronous throw from a `readline` `'line'` handler declared `async`
 *      (the handler's rejection is unhandled — see 1).
 *
 * The guards deliberately KEEP THE PROCESS ALIVE and only report on stderr
 * (which the CLI surfaces in its MCP logs). Exiting is exactly the behaviour
 * that costs the user their session, and a half-working server that still
 * answers `tools/list` beats no server at all. Same reasoning as the main
 * process' `crashGuard.ts`.
 *
 * `withDeadline` is here for the second family of user-visible failures: a tool
 * call that never returns. The bridge clients race a raw `net.connect()` +
 * WebSocket handshake with no timeout of any kind, so a desktop that is up but
 * momentarily blocked (a modal, a long synchronous op) leaves the agent frozen
 * on a tool call forever. Every unbounded wait in these servers must be wrapped.
 *
 * Keep this file dependency-free: it ships as-is inside the packaged Electron
 * app (declared in `package.json` > build > extraResources) and is loaded by
 * `node /path/to/<server>.cjs` through a plain relative `require`.
 */

/**
 * Deadline for reaching the desktop bridge (TCP connect + WS handshake).
 *
 * Generous on purpose: the bridge lives on the Electron main event loop, so a
 * busy desktop can take a moment to answer the upgrade. What matters is that
 * the wait is BOUNDED — an unbounded one is indistinguishable from a hang, and
 * the caller can retry with a freshly-read descriptor once this rejects.
 */
const BRIDGE_CONNECT_TIMEOUT_MS = 10_000;

/**
 * Install the process-level guards. Call it once, at the very top of a server,
 * before any I/O.
 *
 * `serverName` only prefixes the stderr lines so a user reading the CLI's MCP
 * log can tell which of the five servers spoke.
 */
function installStdioGuards(serverName) {
  const tag = `[${serverName}]`;
  const report = (kind, err) => {
    try {
      const detail = err && err.stack ? err.stack : String((err && err.message) || err);
      // eslint-disable-next-line no-console
      console.error(`${tag} ${kind} (kept alive): ${detail}`);
    } catch (_) { /* stderr itself is gone: nothing left to do but survive */ }
  };

  // A dead stdout means the CLI is gone. Swallow the error instead of turning
  // the teardown into a crash; the process exits on its own when stdin closes.
  try { process.stdout.on('error', () => {}); } catch (_) { /* ignore */ }
  try { process.stderr.on('error', () => {}); } catch (_) { /* ignore */ }

  process.on('uncaughtException', (err) => report('uncaught exception', err));
  process.on('unhandledRejection', (reason) => report('unhandled rejection', reason));
}

/**
 * Reject `promise` if it has not settled within `ms`.
 *
 * `onTimeout` is where the caller tears down whatever the promise was waiting
 * on (a half-open socket, typically): without it the abandoned resource keeps
 * the event loop busy and the next attempt races the dead one.
 */
function withDeadline(promise, ms, message, onTimeout) {
  return new Promise((resolve, reject) => {
    let settled = false;
    const timer = setTimeout(() => {
      if (settled) return;
      settled = true;
      try { if (onTimeout) onTimeout(); } catch (_) { /* best-effort cleanup */ }
      reject(new Error(message));
    }, ms);
    promise.then(
      (value) => { if (settled) return; settled = true; clearTimeout(timer); resolve(value); },
      (err) => { if (settled) return; settled = true; clearTimeout(timer); reject(err); },
    );
  });
}

module.exports = { BRIDGE_CONNECT_TIMEOUT_MS, installStdioGuards, withDeadline };
