/**
 * Generic POSIX loopback-port utilities.
 *
 * Extracted verbatim (v2.7 audit reshape) from `backends/opencode-server.ts`,
 * where they were embedded in the backend god-class. They have nothing to do with
 * opencode semantics — they reclaim/await/allocate a 127.0.0.1 port — so they live
 * here as reusable infra. No behavior change from the original.
 */
import { createServer, connect as netConnect } from 'node:net';
import { spawnSync } from 'node:child_process';

/**
 * Reclaim a loopback port a dead child may still hold (lift of openchamber
 * `killProcessOnPort`). Best-effort, POSIX-only (`lsof`/`kill`); a failure is
 * harmless because the next spawn allocates a fresh ephemeral port. Never kills
 * this process. Synchronous + short-timeout so a crash handler doesn't block.
 */
export function reclaimPort(port: number | null): void {
  if (!port || process.platform === 'win32') return;
  try {
    const res = spawnSync('lsof', ['-ti', `:${port}`], { encoding: 'utf8', timeout: 3_000, windowsHide: true });
    const out = res.stdout || '';
    const myPid = process.pid;
    for (const pidStr of out.split(/\s+/)) {
      const pid = parseInt(pidStr.trim(), 10);
      if (pid && pid !== myPid) {
        try {
          spawnSync('kill', ['-9', String(pid)], { stdio: 'ignore', timeout: 2_000 });
        } catch {
          // ignore — best effort
        }
      }
    }
  } catch {
    // lsof absent or failed — the fresh-ephemeral-port spawn doesn't need this.
  }
}

/**
 * Resolve true once nothing is listening on `port` (lift of openchamber
 * `waitForPortRelease`). Used before re-spawning on a fixed port; with ephemeral
 * ports it's a fast no-op. Probes 127.0.0.1; resolves false at the deadline.
 */
export function waitForPortRelease(port: number, timeoutMs: number): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  return new Promise((resolve) => {
    const attempt = () => {
      const socket = netConnect({ port, host: '127.0.0.1' });
      let settled = false;
      const finish = (released: boolean) => {
        if (settled) return;
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (released || Date.now() >= deadline) {
          resolve(released);
          return;
        }
        setTimeout(attempt, 150);
      };
      socket.once('connect', () => finish(false));
      socket.once('error', (err: NodeJS.ErrnoException) => {
        if (err && (err.code === 'ECONNREFUSED' || err.code === 'EHOSTUNREACH')) finish(true);
        else finish(false);
      });
      socket.setTimeout(500, () => finish(true));
    };
    attempt();
  });
}

/** Bind-probe an ephemeral port on loopback. */
export function freePort(): Promise<number> {
  return new Promise((resolve, reject) => {
    const srv = createServer();
    srv.unref();
    srv.on('error', reject);
    srv.listen(0, '127.0.0.1', () => {
      const addr = srv.address();
      if (addr && typeof addr === 'object') {
        const { port } = addr;
        srv.close(() => resolve(port));
      } else {
        srv.close(() => reject(new Error('port-utils: could not determine a free port')));
      }
    });
  });
}
