// Phase F-2-B (B3): fetch a per-world container's X-Olam-Secret via the
// docker-socket-proxy sidecar (container mode) OR via `docker exec` (bare-
// node mode — host-cp running as a plain Node process on the host).
//
// The secret lives at `/tmp/olam-container-secret` inside the world's
// devbox container. Phase E init wrote it (`chmod 400` owned by root —
// world-app user has no write permission, T9 mitigation) and the
// per-world CP's `requireAuth` middleware compares against it. Host CP
// reads the secret server-side and injects `X-Olam-Secret` on proxied
// requests, so the browser never sees the secret directly.
//
// Container mode (`dockerHost = 'tcp://docker-socket-proxy:2375'`):
//   1. POST /containers/<name>/exec
//      body: { Cmd: ['cat', '/tmp/olam-container-secret'], AttachStdout: true, AttachStderr: true }
//      → { Id: '<exec-id>' }
//   2. POST /exec/<exec-id>/start
//      body: { Detach: false, Tty: false }
//      → response stream containing the file bytes (raw multiplexed
//        stdout/stderr per Docker exec protocol)
//
//   The exec endpoint is whitelisted in the socket-proxy (EXEC=1).
//
// Bare-node mode (`dockerHost = 'docker-cli'`):
//   Spawn `docker exec <containerName> cat /tmp/olam-container-secret` via
//   child_process. No socket-proxy on the host; the docker CLI on the
//   operator's $PATH is the canonical access path. Same `olam-<id>-devbox`
//   naming convention applies. ~10 ms of process-spawn overhead per miss
//   is fine because the secret is cached for OLAM_SECRET_CACHE_TTL_SEC
//   (default 300 s).

import { spawnSync } from 'node:child_process';

/**
 * Read /tmp/olam-container-secret from a world's devbox container.
 * Throws on any non-2xx response from the socket-proxy or on the
 * file being empty (the world's CP is misconfigured if it is).
 *
 * @param {object} args
 * @param {string} args.worldId
 * @param {string} args.dockerHost  Either `tcp://...` for socket-proxy
 *   mode or the sentinel `'docker-cli'` for bare-node mode.
 * @param {(host: string, init: RequestInit) => Promise<Response>} [args.fetchImpl]
 *   injectable for tests; defaults to global fetch (Node 22+)
 * @returns {Promise<string>}  the secret (trimmed of trailing whitespace)
 */
export async function fetchContainerSecret({ worldId, dockerHost, fetchImpl = globalThis.fetch }) {
  // Container naming convention: docker provider creates containers as
  // `olam-${worldId}-devbox` (see packages/adapters/src/docker/container.ts).
  // Phase F-2-D dogfood revealed the original `${worldId}-devbox` was
  // missing the `olam-` prefix.
  const containerName = `olam-${worldId}-devbox`;

  // Bare-node mode: shell out to docker exec directly. Operator's docker
  // CLI on $PATH is the canonical access path; no socket-proxy needed.
  if (dockerHost === 'docker-cli') {
    const result = spawnSync(
      'docker',
      ['exec', containerName, 'cat', '/tmp/olam-container-secret'],
      { encoding: 'utf-8' },
    );
    if (result.error) {
      throw new Error(`docker exec ${containerName} cat ... failed: ${result.error.message}`);
    }
    if (result.status !== 0) {
      throw new Error(
        `docker exec ${containerName} cat ... exit ${result.status}: ${(result.stderr || '').trim()}`,
      );
    }
    const secret = (result.stdout || '').trim();
    if (!secret) {
      throw new Error(`/tmp/olam-container-secret empty in container ${containerName}`);
    }
    return secret;
  }

  // Container mode: HTTP via the docker-socket-proxy sidecar.
  // Docker API: tcp://host:port → http://host:port
  const apiBase = dockerHost.replace(/^tcp:\/\//, 'http://');

  // Step 1: create exec instance
  const createUrl = `${apiBase}/containers/${encodeURIComponent(containerName)}/exec`;
  const createRes = await fetchImpl(createUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      Cmd: ['cat', '/tmp/olam-container-secret'],
      AttachStdout: true,
      AttachStderr: true,
      Tty: false,
    }),
  });
  if (!createRes.ok) {
    throw new Error(
      `socket-proxy POST /containers/${containerName}/exec failed: ${createRes.status} ${createRes.statusText}`,
    );
  }
  const createBody = await createRes.json();
  const execId = createBody.Id;
  if (!execId) {
    throw new Error(`socket-proxy /exec did not return Id: ${JSON.stringify(createBody)}`);
  }

  // Step 2: start exec, read stdout. The response is Docker's
  // multiplexed exec stream: 8-byte header per frame + payload bytes.
  // Header byte 0 = stream id (1=stdout, 2=stderr), bytes 4-7 = payload
  // length (big-endian uint32). For `cat <smallfile>` we expect a single
  // frame on stream 1.
  const startUrl = `${apiBase}/exec/${execId}/start`;
  const startRes = await fetchImpl(startUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ Detach: false, Tty: false }),
  });
  if (!startRes.ok) {
    throw new Error(
      `socket-proxy POST /exec/${execId}/start failed: ${startRes.status} ${startRes.statusText}`,
    );
  }
  const buf = new Uint8Array(await startRes.arrayBuffer());

  // Decode the multiplexed stream. Skip stderr frames; concatenate
  // stdout payloads. Empty file → throw (per-world CP is broken).
  const stdoutBytes = decodeDockerExecStream(buf);
  const secret = new TextDecoder('utf-8').decode(stdoutBytes).trim();
  if (!secret) {
    throw new Error(`/tmp/olam-container-secret empty in container ${containerName}`);
  }
  return secret;
}

/**
 * Decode Docker's multiplexed exec stream — keep only stdout (stream id 1).
 * Stream format: each frame is 8-byte header + payload. Header byte 0
 * is the stream id (0=stdin, 1=stdout, 2=stderr); bytes 4-7 are the
 * payload length as big-endian uint32. Bytes 1-3 are reserved (zero).
 *
 * @param {Uint8Array} buf
 * @returns {Uint8Array}
 */
export function decodeDockerExecStream(buf) {
  const out = [];
  let i = 0;
  while (i + 8 <= buf.byteLength) {
    const streamId = buf[i];
    // Big-endian uint32 at offset i+4..i+8
    const len = (buf[i + 4] << 24) | (buf[i + 5] << 16) | (buf[i + 6] << 8) | buf[i + 7];
    const payload = buf.subarray(i + 8, i + 8 + len);
    if (streamId === 1) {
      out.push(payload);
    }
    i += 8 + len;
  }
  // Concatenate.
  let total = 0;
  for (const p of out) total += p.byteLength;
  const merged = new Uint8Array(total);
  let off = 0;
  for (const p of out) {
    merged.set(p, off);
    off += p.byteLength;
  }
  return merged;
}
