/**
 * listening-server-poller.mjs
 * Discovers listening TCP ports inside a world's devbox container.
 * Dual-mode: Docker HTTP API (container) vs docker exec CLI (bare-node).
 * Cache TTL: 10s per world.
 */
import { spawnSync } from 'node:child_process';

const DOCKER_HOST = process.env.DOCKER_HOST ?? 'docker-cli';
// Skip well-known infra ports — these are always running and not user servers
const INFRA_PORTS = new Set([8080, 7681, 7682]);

// Per-world cache: worldId → { ts, servers, error? }
const cache = new Map();
const CACHE_TTL_MS = 10_000;

function worldContainerName(worldId) {
  return `olam-${worldId}-devbox`;
}

/**
 * Parse `ss -tlnp` output into server rows.
 * Output format:
 *   Netid  State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
 *   tcp    LISTEN  0       128     0.0.0.0:5173        0.0.0.0:*           users:(("vite",pid=42,fd=8))
 *
 * @param {string} stdout
 * @returns {Array<{port: number, pid: string, cmd: string}>}
 */
export function parseSsOutput(stdout) {
  const lines = stdout.trim().split('\n').slice(1); // skip header
  const results = [];
  for (const line of lines) {
    const parts = line.trim().split(/\s+/);
    if (parts.length < 5) continue;
    // parts[3] = Local Address:Port  (e.g. "0.0.0.0:5173" or "*:5173" or ":::5173")
    const localAddr = parts[3];
    const colonIdx = localAddr.lastIndexOf(':');
    if (colonIdx === -1) continue;
    const portStr = localAddr.slice(colonIdx + 1);
    const port = parseInt(portStr, 10);
    if (!Number.isFinite(port) || port <= 0) continue;
    if (INFRA_PORTS.has(port)) continue;

    // Extract pid and cmd from process column: users:(("vite",pid=42,fd=8))
    let pid = '';
    let cmd = '';
    const processCol = parts.slice(4).join(' ');
    const pidMatch = /pid=(\d+)/.exec(processCol);
    if (pidMatch) pid = pidMatch[1];
    const cmdMatch = /"([^"]+)"/.exec(processCol);
    if (cmdMatch) cmd = cmdMatch[1];

    results.push({ port, pid, cmd });
  }
  return results;
}

/**
 * Fetch listening servers for a world. Returns cached result if <10s old.
 * @param {string} worldId
 * @returns {Promise<{ts: number, servers: Array<{port: number, pid: string, cmd: string}>, error?: string}>}
 */
export async function getListeningServers(worldId) {
  const cached = cache.get(worldId);
  if (cached && Date.now() - cached.ts < CACHE_TTL_MS) return cached;

  const containerName = worldContainerName(worldId);
  try {
    let stdout;
    if (DOCKER_HOST === 'docker-cli') {
      const result = spawnSync(
        'docker', ['exec', containerName, 'ss', '-tlnp'],
        { encoding: 'utf-8', timeout: 3000 },
      );
      if (result.status !== 0 || result.error) {
        const entry = { ts: Date.now(), servers: [], error: 'container not running' };
        cache.set(worldId, entry);
        return entry;
      }
      stdout = result.stdout ?? '';
    } else {
      const apiBase = DOCKER_HOST.replace(/^tcp:\/\//, 'http://');
      const execCreate = await fetch(
        `${apiBase}/containers/${encodeURIComponent(containerName)}/exec`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            AttachStdout: true,
            AttachStderr: false,
            Cmd: ['ss', '-tlnp'],
          }),
          signal: AbortSignal.timeout(3000),
        },
      );
      if (!execCreate.ok) {
        const entry = { ts: Date.now(), servers: [], error: 'container not running' };
        cache.set(worldId, entry);
        return entry;
      }
      const { Id: execId } = await execCreate.json();
      const execStart = await fetch(`${apiBase}/exec/${execId}/start`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ Detach: false, Tty: false }),
        signal: AbortSignal.timeout(3000),
      });
      // Docker exec start streams multiplexed output (8-byte header per frame)
      const buf = await execStart.arrayBuffer();
      stdout = demuxDockerStream(Buffer.from(buf));
    }
    const servers = parseSsOutput(stdout);
    const entry = { ts: Date.now(), servers };
    cache.set(worldId, entry);
    return entry;
  } catch {
    const entry = { ts: Date.now(), servers: [], error: 'container not running' };
    cache.set(worldId, entry);
    return entry;
  }
}

/**
 * Strip Docker stream multiplexing headers (8 bytes per frame: [stream, 0, 0, 0, size32be]).
 * @param {Buffer} buf
 * @returns {string}
 */
function demuxDockerStream(buf) {
  let output = '';
  let offset = 0;
  while (offset + 8 <= buf.length) {
    const size = buf.readUInt32BE(offset + 4);
    const payload = buf.slice(offset + 8, offset + 8 + size);
    output += payload.toString('utf-8');
    offset += 8 + size;
  }
  return output;
}

export { parseSsOutput as _parseSsOutputForTests };
