'use strict';

const fs = require('fs');
const http = require('http');
const net = require('net');
const os = require('os');
const path = require('path');
const { spawn, execFile } = require('child_process');
const { processTitle: formatProcessTitle, processTitleCandidates } = require('./process-title');
const {
  checkWalleNativeDeps,
  rebuildWalleNativeDeps,
  shouldRebuildNativeDeps,
} = require('./walle-native-health');

const RESTART_DELAY_MS = 5000;
const RESTART_BURST = 5;
const RESTART_WINDOW_MS = 300000;

function writeJson(res, statusCode, payload) {
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(payload));
}

function execFileText(file, args) {
  return new Promise((resolve) => {
    execFile(file, args, { timeout: 2000, maxBuffer: 1024 * 1024 }, (err, stdout) => {
      resolve(err ? '' : String(stdout || ''));
    });
  });
}

function parsePsOutput(stdout) {
  const rows = [];
  for (const line of String(stdout || '').split(/\r?\n/)) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    const match = trimmed.match(/^(\d+)\s+(.+)$/);
    if (!match) continue;
    rows.push({ pid: Number(match[1]), command: match[2].trim() });
  }
  return rows;
}

async function findPidsByExactCommand(commandName) {
  const stdout = await execFileText('ps', ['-axo', 'pid=,command=']);
  return parsePsOutput(stdout)
    .filter(row => row.command === commandName)
    .map(row => row.pid);
}

async function findListeningPidsByExactCommand(port, commandName) {
  const stdout = await execFileText('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN', '-nP']);
  const pids = [...new Set(String(stdout || '')
    .split(/\s+/)
    .map(s => Number(s))
    .filter(Number.isInteger))];
  if (pids.length === 0) return [];

  const psOut = await execFileText('ps', ['-p', pids.join(','), '-o', 'pid=,command=']);
  return parsePsOutput(psOut)
    .filter(row => row.command === commandName)
    .map(row => row.pid);
}

const defaultProcessOps = {
  pid: process.pid,
  kill: (pid, signal) => process.kill(pid, signal),
  findPidsByExactCommand,
  findListeningPidsByExactCommand,
};

function createWalleSupervisor({
  ctmDir,
  configDir,
  ctmPort = process.env.CTM_PORT || 3456,
  wallePort,
  instanceTag,
  walleToken = null,
  telemetry = { track() {} },
  isDisabled = () => !!process.env.WALLE_DISABLED,
  processOps = defaultProcessOps,
  nativeDeps = { check: checkWalleNativeDeps, rebuild: rebuildWalleNativeDeps, shouldRebuild: shouldRebuildNativeDeps },
  spawnProcess = spawn,
  nativeCheckIntervalMs = 60000,
  mcpReadyTimeoutMs = 30000,
  mcpReadyRetryMs = 250,
  mcpReadyProbe = null,
  // How often getStatus() may re-probe a running Wall-E whose last verdict is
  // not-ready, to self-heal a stale startup timeout (a too-short probe vs a slow
  // brain boot). Throttled so frequent status polls don't hammer the MCP endpoint.
  mcpReadySelfHealIntervalMs = 3000,
}) {
  let intentionallyStopped = false;
  let starting = false;
  let child = null;
  let childPid = null;
  let restartHistory = [];
  let lastNativeHealth = null;
  let lastNativeHealthAt = 0;
  let lastStartError = null;
  let lastMcpReady = null;
  let lastSelfHealProbeAt = 0;

  const pidFile = () => path.join(configDir, 'walle.pid');
  const processTitle = () => formatProcessTitle('walle', instanceTag || 'primary');
  const processTitles = () => processTitleCandidates('walle', instanceTag || 'primary');
  const walleDir = () => path.join(ctmDir, '..', 'wall-e');

  function _nativeErrorPayload(health) {
    const h = health || lastNativeHealth || {};
    return {
      ok: false,
      code: h.code || 'walle_native_dependency_failed',
      message: h.message || 'Wall-E native dependency check failed.',
      nativeDependency: h,
    };
  }

  function requestMcpJson(message, { timeoutMs = 1000 } = {}) {
    const body = JSON.stringify(message);
    return new Promise((resolve, reject) => {
      const req = http.request({
        hostname: '127.0.0.1',
        port: wallePort,
        path: '/mcp',
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(body),
          'MCP-Protocol-Version': '2025-06-18',
          ...(walleToken ? { Authorization: `Bearer ${walleToken}` } : {}),
        },
        timeout: timeoutMs,
      }, (res) => {
        let raw = '';
        res.setEncoding('utf8');
        res.on('data', chunk => { raw += chunk; });
        res.on('end', () => {
          if (res.statusCode < 200 || res.statusCode >= 300) {
            reject(new Error(`Wall-E MCP returned HTTP ${res.statusCode}: ${raw.slice(0, 300)}`));
            return;
          }
          try {
            const parsed = raw.trim() ? JSON.parse(raw) : null;
            if (parsed?.error) reject(new Error(`Wall-E MCP JSON-RPC error: ${parsed.error.message || parsed.error.code}`));
            else resolve(parsed);
          } catch (err) {
            reject(new Error(`Wall-E MCP returned invalid JSON: ${err.message}`));
          }
        });
      });
      req.on('timeout', () => req.destroy(new Error('Wall-E MCP readiness request timed out')));
      req.on('error', reject);
      req.end(body);
    });
  }

  async function defaultMcpReadyProbe() {
    const init = await requestMcpJson({
      jsonrpc: '2.0',
      id: 1,
      method: 'initialize',
      params: {
        protocolVersion: '2025-06-18',
        capabilities: {},
        clientInfo: { name: 'ctm-walle-supervisor', version: '1.0.0' },
      },
    });
    if (init?.result?.serverInfo?.name !== 'wall-e') {
      throw new Error('Wall-E MCP initialize did not return serverInfo.name=wall-e');
    }
    const tools = await requestMcpJson({
      jsonrpc: '2.0',
      id: 2,
      method: 'tools/list',
      params: {},
    });
    const list = tools?.result?.tools;
    if (!Array.isArray(list) || !list.some(tool => tool?.name === 'walle_memory_status')) {
      throw new Error('Wall-E MCP tools/list did not include walle_memory_status');
    }
    const status = await requestMcpJson({
      jsonrpc: '2.0',
      id: 3,
      method: 'tools/call',
      params: { name: 'walle_memory_status', arguments: {} },
    });
    const result = status?.result || {};
    if (result.isError) {
      throw new Error('Wall-E MCP walle_memory_status returned an error result');
    }
    let structured = result.structuredContent;
    if (!structured && Array.isArray(result.content)) {
      const text = result.content.find(item => item?.type === 'text' && item.text)?.text;
      if (text) {
        try { structured = JSON.parse(text); } catch {}
      }
    }
    if (!structured || typeof structured !== 'object') {
      throw new Error('Wall-E MCP walle_memory_status did not return structured health');
    }
    if (structured.ok === false) {
      throw new Error(`Wall-E MCP walle_memory_status is not healthy: ${structured.code || structured.error || 'unknown'}`);
    }
    return { ok: true, toolCount: list.length, memoryStatusOk: true };
  }

  async function waitForMcpReady(pid, source, options = {}) {
    const timeoutMs = Math.max(0, Number(options.timeoutMs ?? mcpReadyTimeoutMs));
    const retryMs = Math.max(25, Number(options.retryMs ?? mcpReadyRetryMs));
    const deadline = Date.now() + timeoutMs;
    const startedAt = Date.now();
    const probe = mcpReadyProbe || defaultMcpReadyProbe;
    let attempts = 0;
    let lastErr = null;

    while (attempts === 0 || Date.now() <= deadline) {
      attempts += 1;
      if (pid && !isAlive(pid)) {
        lastMcpReady = {
          ok: false,
          code: 'walle_exited_before_mcp_ready',
          message: `Wall-E process ${pid} exited before MCP became ready.`,
          pid,
          source,
          attempts,
          elapsedMs: Date.now() - startedAt,
        };
        lastStartError = {
          ok: false,
          code: lastMcpReady.code,
          message: lastMcpReady.message,
        };
        return lastMcpReady;
      }
      try {
        const result = await probe({
          pid,
          port: wallePort,
          token: walleToken,
          source,
          requestMcpJson,
        });
        if (result && typeof result === 'object' && result.ok === false) {
          const probeErr = new Error(result.message || result.error || result.code || 'Wall-E MCP readiness probe failed');
          probeErr.code = result.code || 'walle_mcp_ready_failed';
          throw probeErr;
        }
        lastMcpReady = {
          ok: true,
          code: 'ok',
          message: 'Wall-E MCP is ready.',
          pid,
          source,
          attempts,
          elapsedMs: Date.now() - startedAt,
          ...(result && typeof result === 'object' ? result : {}),
        };
        lastStartError = null;
        return lastMcpReady;
      } catch (err) {
        lastErr = err;
      }
      if (Date.now() > deadline) break;
      await sleep(retryMs);
    }

    lastMcpReady = {
      ok: false,
      code: 'walle_mcp_ready_timeout',
      message: `Wall-E process ${pid || 'unknown'} did not become MCP-ready within ${timeoutMs}ms: ${lastErr?.message || 'unknown error'}`,
      pid: pid || null,
      source,
      attempts,
      elapsedMs: Date.now() - startedAt,
      lastError: lastErr ? { message: lastErr.message, code: lastErr.code || '' } : null,
    };
    lastStartError = {
      ok: false,
      code: lastMcpReady.code,
      message: lastMcpReady.message,
    };
    try {
      telemetry.track('walle_mcp_ready_failed', {
        source: source || 'unknown',
        code: lastMcpReady.code,
        attempts,
        elapsed_ms: lastMcpReady.elapsedMs,
        message: String(lastMcpReady.message || '').slice(0, 240),
      });
    } catch {}
    return lastMcpReady;
  }

  async function getNativeDependencyStatus({ force = false } = {}) {
    const now = Date.now();
    if (!force && lastNativeHealth && now - lastNativeHealthAt < nativeCheckIntervalMs) {
      return lastNativeHealth;
    }
    try {
      lastNativeHealth = await nativeDeps.check({
        walleDir: walleDir(),
        nodePath: process.execPath,
      });
    } catch (err) {
      lastNativeHealth = {
        ok: false,
        code: 'native_dependency_check_failed',
        message: err.message || String(err),
        error: { message: err.message || String(err) },
      };
    }
    lastNativeHealthAt = now;
    return lastNativeHealth;
  }

  async function ensureNativeDependenciesReady(source, { allowRebuild = true } = {}) {
    let health = await getNativeDependencyStatus({ force: true });
    if (health.ok) {
      lastStartError = null;
      return { ok: true, health };
    }

    if (allowRebuild && nativeDeps.shouldRebuild(health)) {
      let rebuild = null;
      try {
        rebuild = await nativeDeps.rebuild({
          walleDir: walleDir(),
          nodePath: process.execPath,
        });
      } catch (err) {
        rebuild = {
          ok: false,
          code: 'native_dependency_rebuild_failed',
          message: err.message || String(err),
        };
      }
      health = {
        ...(await getNativeDependencyStatus({ force: true })),
        rebuild_attempted: true,
        rebuild,
      };
    }

    if (!health.ok) {
      lastStartError = _nativeErrorPayload(health);
      try {
        telemetry.track('walle_native_dependency_failed', {
          source: source || 'unknown',
          code: health.code || 'unknown',
          message: String(health.message || '').slice(0, 240),
          node: health.node || {},
          rebuild_attempted: !!health.rebuild_attempted,
          rebuild_ok: !!health.rebuild?.ok,
        });
      } catch {}
      return { ok: false, health };
    }

    lastStartError = null;
    return { ok: true, health };
  }

  async function statusWithDiagnostics(base, { checkNative = false } = {}) {
    let nativeDependency = lastNativeHealth;
    if (checkNative) nativeDependency = await getNativeDependencyStatus({ force: false });
    // Self-heal a stale MCP-not-ready verdict. A startup probe can time out before a
    // large brain finishes booting (e.g. the first daily-backup VACUUM), leaving a
    // sticky false "MCP never became ready" even though the daemon is now serving. If
    // the process is running but the last verdict is not-ok, re-probe (throttled) — a
    // successful mcpReadyProbeOnce() clears it; a failed probe leaves the verdict intact.
    if (base.running && base.pid && (!lastMcpReady || lastMcpReady.ok === false)) {
      const now = Date.now();
      if (now - lastSelfHealProbeAt >= mcpReadySelfHealIntervalMs) {
        lastSelfHealProbeAt = now;
        try { await mcpReadyProbeOnce({ pid: base.pid, source: 'watchdog', timeoutMs: 2500 }); } catch {}
      }
    }
    const stoppedNativeFailure = nativeDependency && nativeDependency.ok === false ? nativeDependency : null;
    const effectiveError = base.running ? null : (lastStartError || (stoppedNativeFailure ? _nativeErrorPayload(stoppedNativeFailure) : null));
    return {
      ...base,
      code: effectiveError ? effectiveError.code : null,
      error: effectiveError ? effectiveError.message : null,
      nativeDependency: nativeDependency || null,
      lastStartError: lastStartError || null,
      mcpReady: lastMcpReady || null,
    };
  }

  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  function waitForPortFree() {
    return new Promise((resolve) => {
      let waited = 0;
      const poll = () => {
        const sock = net.createConnection({ port: wallePort, host: '127.0.0.1' }, () => {
          sock.destroy();
          waited += 200;
          if (waited >= 3000) return resolve();
          setTimeout(poll, 200);
        });
        sock.on('error', () => { sock.destroy(); resolve(); });
        sock.setTimeout(500, () => { sock.destroy(); resolve(); });
      };
      poll();
    });
  }

  async function collectManagedPids() {
    const myPid = processOps.pid || process.pid;
    const pidSet = new Set();
    if (childPid) pidSet.add(childPid);

    try {
      const savedPid = parseInt(fs.readFileSync(pidFile(), 'utf8').trim(), 10);
      if (savedPid && savedPid !== myPid) pidSet.add(savedPid);
    } catch {}

    const discovered = await Promise.all(processTitles().flatMap(title => [
      processOps.findPidsByExactCommand ? processOps.findPidsByExactCommand(title) : [],
      processOps.findListeningPidsByExactCommand ? processOps.findListeningPidsByExactCommand(wallePort, title) : [],
    ]));
    for (const p of discovered.flat()) {
      if (p && p !== myPid) pidSet.add(p);
    }

    for (const p of [...pidSet]) {
      try { processOps.kill(p, 0); } catch { pidSet.delete(p); }
    }
    return pidSet;
  }

  function readPidFile() {
    try {
      const pid = parseInt(fs.readFileSync(pidFile(), 'utf8').trim(), 10);
      return pid || null;
    } catch {
      return null;
    }
  }

  function isAlive(pid) {
    if (!pid || pid === (processOps.pid || process.pid)) return false;
    try {
      processOps.kill(pid, 0);
      return true;
    } catch {
      return false;
    }
  }

  function writePidFile(pid) {
    try {
      fs.mkdirSync(configDir, { recursive: true });
      fs.writeFileSync(pidFile(), String(pid));
      return true;
    } catch {
      return false;
    }
  }

  function removePidFile() {
    try {
      fs.unlinkSync(pidFile());
      return true;
    } catch {
      return false;
    }
  }

  async function killAll() {
    const pidSet = await collectManagedPids();
    if (pidSet.size === 0) return waitForPortFree();

    for (const p of pidSet) {
      try { processOps.kill(p, 'SIGTERM'); } catch {}
    }

    let waited = 0;
    while (waited < 2000) {
      await new Promise(resolve => setTimeout(resolve, waited === 0 ? 50 : 100));
      waited += waited === 0 ? 50 : 100;
      const alive = [];
      for (const p of pidSet) {
        try { processOps.kill(p, 0); alive.push(p); } catch {}
      }
      if (alive.length === 0) {
        child = null;
        childPid = null;
        return waitForPortFree();
      }
    }

    for (const p of pidSet) {
      try { processOps.kill(p, 'SIGKILL'); } catch {}
    }
    child = null;
    childPid = null;
    return waitForPortFree();
  }

  async function stopForProcessExit(reason = 'process-exit') {
    intentionallyStopped = true;
    try { telemetry.track('walle_stop_for_process_exit', { reason, port: wallePort }); } catch {}
    return killAll();
  }

  function inCrashLoop() {
    const now = Date.now();
    restartHistory = restartHistory.filter(t => now - t < RESTART_WINDOW_MS);
    return restartHistory.length >= RESTART_BURST;
  }

  function spawnWalle(source) {
    const dir = walleDir();
    const agentScript = path.join(dir, 'agent.js');
    let stderrTarget = 'ignore';
    let crashLogFd = null;
    try {
      const crashLogDir = path.join(os.homedir(), '.walle', 'logs');
      fs.mkdirSync(crashLogDir, { recursive: true });
      crashLogFd = fs.openSync(path.join(crashLogDir, 'crash.log'), 'a');
      stderrTarget = crashLogFd;
    } catch {}

    const childEnv = {
      ...process.env,
      CTM_PORT: String(ctmPort),
      CTM_API_BASE_URL: `http://127.0.0.1:${ctmPort}`,
      WALL_E_PORT: String(wallePort),
      WALLE_INSTANCE_TAG: instanceTag,
      WALL_E_PROCESS_ROLE: 'walle-daemon',
    };
    delete childEnv.CTM_PROCESS_ROLE;
    if (walleToken) childEnv.WALL_E_API_TOKEN = walleToken;

    const nextChild = spawnProcess(
      process.execPath,
      [agentScript],
      {
        cwd: dir,
        detached: true,
        stdio: ['ignore', 'ignore', stderrTarget],
        env: childEnv,
        argv0: processTitle(),
      }
    );
    nextChild.unref();

    child = nextChild;
    childPid = nextChild.pid;
    try { fs.writeFileSync(pidFile(), String(nextChild.pid)); } catch {}
    try { telemetry.track('walle_spawn', { pid: nextChild.pid, port: wallePort, source: source || 'unknown' }); } catch {}

    const myPid = nextChild.pid;
    nextChild.on('exit', (code, signal) => {
      if (childPid !== myPid) return;
      child = null;
      childPid = null;
      if (intentionallyStopped) return;

      restartHistory.push(Date.now());
      if (inCrashLoop()) {
        console.error(`[wall-e] Crash loop detected (${RESTART_BURST} restarts in ${RESTART_WINDOW_MS / 1000}s) - giving up. Use /api/start/walle to retry.`);
        lastStartError = {
          ok: false,
          code: 'walle_crash_loop',
          message: `Wall-E crashed ${RESTART_BURST} times in ${RESTART_WINDOW_MS / 1000}s and the supervisor stopped retrying.`,
        };
        try { telemetry.track('walle_crash_loop', { lastPid: myPid, code, signal }); } catch {}
        intentionallyStopped = true;
        return;
      }

      console.log(`[wall-e] Process ${myPid} exited (code=${code}, signal=${signal}) - restarting in ${RESTART_DELAY_MS / 1000}s...`);
      try { telemetry.track('walle_exit', { pid: myPid, code, signal, source: 'auto-restart' }); } catch {}

      setTimeout(() => {
        if (intentionallyStopped || isDisabled()) return;
        const newPid = spawnWalle('auto-restart');
        console.log(`[wall-e] Restarted (PID ${newPid})`);
      }, RESTART_DELAY_MS);
    });

    return nextChild.pid;
  }

  async function getStatus() {
    const savedPid = readPidFile();
    let stalePid = null;

    if (childPid && isAlive(childPid)) {
      return statusWithDiagnostics({
        running: true,
        pid: childPid,
        pidSource: 'child',
        stalePid: savedPid && savedPid !== childPid ? savedPid : null,
        repairedPidFile: savedPid !== childPid ? writePidFile(childPid) : false,
      });
    }

    if (savedPid && isAlive(savedPid)) {
      return statusWithDiagnostics({ running: true, pid: savedPid, pidSource: 'pid_file' });
    }
    if (savedPid) stalePid = savedPid;

    let discovered = [];
    try {
      const results = await Promise.all(processTitles().flatMap(title => [
        processOps.findPidsByExactCommand ? processOps.findPidsByExactCommand(title) : [],
        processOps.findListeningPidsByExactCommand ? processOps.findListeningPidsByExactCommand(wallePort, title) : [],
      ]));
      discovered = [...new Set(results.flat().filter(pid => pid && isAlive(pid)))];
    } catch {}

    if (discovered.length > 0) {
      const pid = discovered[0];
      return statusWithDiagnostics({
        running: true,
        pid,
        pidSource: 'discovered',
        stalePid,
        repairedPidFile: writePidFile(pid),
      });
    }

    return statusWithDiagnostics({
      running: false,
      pid: null,
      pidSource: stalePid ? 'stale_pid_file' : 'none',
      stalePid,
      removedStalePidFile: stalePid ? removePidFile() : false,
    }, { checkNative: true });
  }

  function apiStop(req, res) {
    intentionallyStopped = true;
    try { telemetry.track('walle_stop', { port: wallePort }); } catch {}
    killAll().then(() => writeJson(res, 200, { ok: true, message: 'Wall-E stopped' }));
  }

  async function apiStart(req, res) {
    if (starting) {
      return writeJson(res, 409, { ok: false, message: 'Wall-E start already in progress' });
    }
    starting = true;
    intentionallyStopped = false;
    restartHistory = [];
    try {
      if (childPid && isAlive(childPid)) {
        const readyStatus = await mcpReadyProbeOnce({ pid: childPid, source: 'start_api_existing', timeoutMs: 2500 });
        if (readyStatus.ok) {
          return writeJson(res, 200, {
            ok: true,
            message: 'Wall-E is already running and MCP-ready on port ' + wallePort,
            pid: childPid,
            nativeDependency: lastNativeHealth || null,
            mcpReady: readyStatus,
          });
        }
        return writeJson(res, 503, {
          ok: false,
          code: readyStatus.code,
          message: readyStatus.message,
          pid: childPid,
          nativeDependency: lastNativeHealth || null,
          mcpReady: readyStatus,
        });
      }

      const ready = await ensureNativeDependenciesReady('start_api');
      if (!ready.ok) {
        return writeJson(res, 503, {
          ..._nativeErrorPayload(ready.health),
          ok: false,
          message: `Wall-E cannot start because its native dependencies are not loadable: ${ready.health.message}`,
        });
      }

      let orphanPid = null;
      try {
        const saved = parseInt(fs.readFileSync(pidFile(), 'utf8').trim(), 10);
        if (saved && isAlive(saved)) orphanPid = saved;
      } catch {}

      if (orphanPid) {
        console.log(`[wall-e] Found orphan process on port ${wallePort}: ${orphanPid} - killing before fresh start`);
        try { processOps.kill(orphanPid, 'SIGTERM'); } catch {}
        await sleep(3000);
        try {
          processOps.kill(orphanPid, 0);
          processOps.kill(orphanPid, 'SIGKILL');
        } catch {}
        await sleep(2000);
        const pid = spawnWalle('start_api_after_orphan_kill');
        const readyStatus = await waitForMcpReady(pid, 'start_api_after_orphan_kill');
        if (readyStatus.ok) {
          return writeJson(res, 200, {
            ok: true,
            message: 'Wall-E started and MCP is ready on port ' + wallePort + ' (killed orphan).',
            pid,
            nativeDependency: ready.health,
            mcpReady: readyStatus,
          });
        }
        return writeJson(res, 503, {
          ok: false,
          code: readyStatus.code,
          message: readyStatus.message,
          pid,
          nativeDependency: ready.health,
          mcpReady: readyStatus,
        });
      }

      const pid = spawnWalle('start_api');
      const readyStatus = await waitForMcpReady(pid, 'start_api');
      if (readyStatus.ok) {
        return writeJson(res, 200, {
          ok: true,
          message: 'Wall-E started and MCP is ready on port ' + wallePort + '.',
          pid,
          nativeDependency: ready.health,
          mcpReady: readyStatus,
        });
      }
      return writeJson(res, 503, {
        ok: false,
        code: readyStatus.code,
        message: readyStatus.message,
        pid,
        nativeDependency: ready.health,
        mcpReady: readyStatus,
      });
    } catch (err) {
      lastStartError = {
        ok: false,
        code: 'walle_spawn_failed',
        message: err.message || String(err),
      };
      return writeJson(res, 500, lastStartError);
    } finally {
      starting = false;
    }
  }

  function restartQuiet() {
    ensureNativeDependenciesReady('config_change').then((ready) => {
      if (!ready.ok) {
        console.error(`[setup] Wall-E restart blocked: ${ready.health.message}`);
        return null;
      }
      intentionallyStopped = true;
      return killAll().then(() => sleep(2000)).then(() => {
        intentionallyStopped = false;
        const pid = spawnWalle('config_change');
        return waitForMcpReady(pid, 'config_change').then((readyStatus) => {
          if (readyStatus.ok) {
            console.log('[setup] Restarted Wall-E (PID ' + pid + ') and MCP is ready to pick up new API config');
          } else {
            console.error(`[setup] Wall-E restarted for config change but MCP readiness failed: ${readyStatus.message}`);
          }
          return pid;
        });
      });
    }).catch((err) => {
      intentionallyStopped = false;
      lastStartError = {
        ok: false,
        code: 'walle_restart_failed',
        message: err.message || String(err),
      };
      console.error(`[setup] Wall-E restart failed: ${lastStartError.message}`);
    });
  }

  async function apiRestart(req, res) {
    const ready = await ensureNativeDependenciesReady('restart_api');
    if (!ready.ok) {
      return writeJson(res, 503, {
        ..._nativeErrorPayload(ready.health),
        ok: false,
        message: `Wall-E cannot restart because its native dependencies are not loadable: ${ready.health.message}`,
      });
    }
    intentionallyStopped = true;
    try {
      await killAll();
      intentionallyStopped = false;
      const pid = spawnWalle('restart_api');
      const readyStatus = await waitForMcpReady(pid, 'restart_api');
      if (readyStatus.ok) {
        return writeJson(res, 200, {
          ok: true,
          message: 'Wall-E restarted and MCP is ready.',
          pid,
          nativeDependency: ready.health,
          mcpReady: readyStatus,
        });
      }
      return writeJson(res, 503, {
        ok: false,
        code: readyStatus.code,
        message: readyStatus.message,
        pid,
        nativeDependency: ready.health,
        mcpReady: readyStatus,
      });
    } catch (err) {
      intentionallyStopped = false;
      lastStartError = {
        ok: false,
        code: 'walle_restart_failed',
        message: err.message || String(err),
      };
      return writeJson(res, 500, lastStartError);
    }
  }

  // One-shot, no-retry MCP readiness probe for external health watchdogs.
  // Unlike waitForMcpReady (which loops until a deadline), this performs a
  // single probe. A successful probe is authoritative enough to clear stale
  // startup/MCP errors left behind by an earlier timeout; otherwise service
  // status can keep reporting a dead Wall-E after the daemon has recovered.
  async function mcpReadyProbeOnce({ timeoutMs = 2000, source = 'watchdog', pid = childPid } = {}) {
    const probe = mcpReadyProbe || defaultMcpReadyProbe;
    try {
      const result = await probe({
        pid,
        port: wallePort,
        token: walleToken,
        source,
        requestMcpJson: (message, opts = {}) => requestMcpJson(message, { timeoutMs, ...opts }),
      });
      if (result && typeof result === 'object' && result.ok === false) {
        return { ok: false, code: result.code || 'walle_mcp_ready_failed', message: result.message || result.error || 'Wall-E MCP readiness probe failed' };
      }
      lastMcpReady = { ok: true, code: 'ok', message: 'Wall-E MCP is ready.', pid, source, ...(result && typeof result === 'object' ? result : {}) };
      lastStartError = null;
      return lastMcpReady;
    } catch (err) {
      return { ok: false, code: err.code || 'walle_mcp_probe_failed', message: err.message || String(err), pid, source };
    }
  }

  async function bootStart() {
    if (intentionallyStopped || isDisabled()) return Promise.resolve(null);
    const ready = await ensureNativeDependenciesReady('boot');
    if (!ready.ok) {
      console.error(`[wall-e] Boot start blocked: ${ready.health.message}`);
      return null;
    }
    await killAll();
    const pid = spawnWalle('boot');
    waitForMcpReady(pid, 'boot').then((readyStatus) => {
      if (!readyStatus.ok) {
        console.error(`[wall-e] Boot start MCP readiness failed: ${readyStatus.message}`);
      }
    }).catch((err) => {
      console.error(`[wall-e] Boot start MCP readiness check threw: ${err.message}`);
    });
    return pid;
  }

  return {
    apiRestart,
    apiStart,
    apiStop,
    bootStart,
    getStatus,
    getNativeDependencyStatus,
    isIntentionallyStopped: () => intentionallyStopped,
    killAll,
    mcpReadyProbeOnce,
    restartQuiet,
    stopForProcessExit,
    spawnWalle,
    waitForMcpReady,
  };
}

module.exports = { createWalleSupervisor };
