// serve-only-config.mjs — host-cp SERVE-ONLY mode gate (Phase A of
// host-cp-gke-serve-only-mode).
//
// host-cp normally runs as a local operator sidecar coupled to the host's
// docker daemon + operator-repo + gh-config. On a managed GKE cluster those
// host-couplings are absent: host-cp only serves plan-chat-spa + the
// host-native `/api/*` surface; world orchestration runs elsewhere.
//
// `OLAM_HOST_CP_SERVE_ONLY=true` switches host-cp into that degraded shape:
//   - no docker transport connect, no world discovery
//   - no PlanOrchestrator docker wiring, no pr-merge-poller docker/repo deps
//   - world-orchestration routes (`/api/world/*`) return a structured 503
//   - version-status degrades to 'unknown' (no operator-repo)
//
// The flag defaults OFF — the local docker/k3d FULL mode is byte-for-byte
// unchanged. This module is a tiny pure seam so the gate decision can be
// unit-tested WITHOUT booting server.mjs (which connects docker + binds a
// port at module load and therefore can't be imported in a test).
//
// ONE coarse flag — no granular per-subsystem toggles (plan S1 / YAGNI).

/**
 * Decide whether host-cp runs in SERVE-ONLY mode.
 *
 * Strict `=== 'true'` parse (mirrors the HOST_CP_MODE env-flag convention
 * in server.mjs): only the literal string `'true'` enables it. Any other
 * value — unset, `'1'`, `'false'`, `''`, `'TRUE'` — keeps FULL mode so the
 * default stays OFF and operators can't half-enable it by accident.
 *
 * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
 *   Environment to read `OLAM_HOST_CP_SERVE_ONLY` from. Defaults to
 *   `process.env`.
 * @returns {boolean} `true` when serve-only mode is active.
 */
export function isServeOnly(env = process.env) {
  return env?.OLAM_HOST_CP_SERVE_ONLY === 'true';
}

/**
 * Structured 503 body for world-orchestration routes that are unavailable
 * in serve-only mode. Reuses the host-cp `/api/*` JSON-error shape
 * (`{ error, message }`) so SPA error handling treats it uniformly.
 *
 * @type {{ error: 'orchestration_unavailable', message: string }}
 */
export const ORCHESTRATION_UNAVAILABLE = Object.freeze({
  error: 'orchestration_unavailable',
  message:
    'host-cp is in serve-only mode (managed cluster); world orchestration runs elsewhere',
});

/**
 * True when `pathname` (+ `method`) is a world-ORCHESTRATION route that must
 * degrade to a structured 503 in serve-only mode. The surface is wider than
 * the singular `/api/world/` proxy: it also covers the plural `/api/worlds/`
 * per-world mutation/read routes (e.g. `POST /api/worlds/<id>/tunnels` which
 * spawns a real cloudflare tunnel, `DELETE /api/worlds/<id>` which destroys a
 * world), world creation (`POST /api/worlds`), and the CLI `/v1/worlds/`
 * routes. Without this breadth a serve-only host-cp on a shared cluster would
 * execute tunnel/destroy mutations — the opposite of honest degradation.
 * (CP3 finding: the singular-only guard let POST /api/worlds/<id>/tunnels
 * open a live public tunnel in serve-only.)
 *
 * Deliberately NOT orchestration: `GET`/`HEAD /api/worlds` (the bare LIST
 * endpoint) — it returns an empty array in serve-only, which is honest.
 *
 * @param {unknown} pathname  URL.pathname (no querystring).
 * @param {string} [method]   HTTP method (defaults 'GET').
 * @returns {boolean}
 */
export function isOrchestrationRoute(pathname, method = 'GET') {
  if (typeof pathname !== 'string') return false;
  // Singular /api/world/<id>/... — the per-world CP proxy + /progress.
  if (pathname.startsWith('/api/world/')) return true;
  // CLI per-world routes (olam status/logs <world>).
  if (pathname.startsWith('/v1/worlds/')) return true;
  // Plural /api/worlds:
  //   bare LIST (GET/HEAD /api/worlds) → honest [] in serve-only, NOT blocked.
  //   create (POST /api/worlds) + any per-world subpath (/api/worlds/<id>...) → 503.
  if (pathname === '/api/worlds') {
    return method !== 'GET' && method !== 'HEAD';
  }
  if (/^\/api\/worlds\/[^/?#]+/.test(pathname)) return true;
  return false;
}
