// config-reader.mjs — Phase D (olam-config-store-unification): a host-cp-local,
// DEPENDENCY-FREE reader for a single dotted value out of `config.json`.
//
// # Why a copy lives here (not an `@olam/core` import)
//
// host-cp is a pure `.mjs` package with NO `@olam/core` dependency — it cannot
// import the TypeScript cloud-state resolver, and a relative reach into
// `packages/core/src/...` would (a) couple host-cp to core's source layout and
// (b) fail to resolve in the published/container build where core is not a
// sibling on disk. The canonical zero-dep reader is
// `packages/core/src/cloud-state/read-config-value.mjs`; this module INLINES the
// same logic (Phase D tracker explicitly permits copy-inlining the tiny reader)
// and adds host-cp's container-aware `config.json` directory resolution.
//
// # Container path resolution
//
// host-cp reads operator state from a bind-mount, NOT from `~/.olam` directly:
// compose.yaml mounts `${HOME}/.olam → /data`, so inside the container the
// canonical config lives at `/data/config.json` (os.homedir() → /root, which is
// the ephemeral container layer — the WRONG place, the same bug fixed for
// plan.db / plan-chat-secret). `olamConfigDir()` resolves the directory holding
// `config.json` honouring, in order:
//   1. process.env.OLAM_HOME              (explicit override — D2 requirement)
//   2. '/data' when HOST_CP_MODE==='container'  (the compose bind-mount target)
//   3. path.join(os.homedir(), '.olam')   (bare-node install — no behaviour change)
//
// Returns the resolved value or `null` (file absent, bad JSON, or path miss) —
// NEVER throws, so a fail-open caller degrades gracefully to its legacy legs.

import { readFileSync, existsSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';

/**
 * Deployment-mode detection, identical to server.mjs: container when an
 * explicit OLAM_HOST_CP_MODE says so, else auto-detected from `/.dockerenv`
 * (created by the docker runtime on container start). Re-derived here (rather
 * than imported from server.mjs) so this module has no server.mjs dependency —
 * server.mjs imports THIS, not the reverse.
 *
 * @returns {'container' | 'bare'}
 */
function hostCpMode() {
  return (
    process.env.OLAM_HOST_CP_MODE ??
    (existsSync('/.dockerenv') ? 'container' : 'bare')
  );
}

/**
 * Resolve the directory that CONTAINS `config.json` (the `~/.olam` ROOT, or its
 * container `/data` equivalent). Re-reads process.env on every call so a direnv
 * org-switch or a late OLAM_HOME export is observed (no module-load capture).
 *
 * @returns {string}
 */
export function olamConfigDir() {
  const olamHome = process.env.OLAM_HOME;
  if (olamHome && olamHome.length > 0) return olamHome;
  if (hostCpMode() === 'container') return '/data';
  return path.join(os.homedir(), '.olam');
}

/**
 * Absolute path to the canonical `config.json` host-cp reads.
 * @returns {string}
 */
export function configJsonPath() {
  return path.join(olamConfigDir(), 'config.json');
}

/**
 * Read a dotted path (e.g. `cloud.urls.anthropic-base-url`) out of the
 * container/host `config.json`. Mirrors
 * packages/core/src/cloud-state/read-config-value.mjs: only `.` splits
 * segments (dash-containing keys like `kg-proxy-url` are fine), and any
 * miss / read error / corrupt JSON returns `null` (fail-open). Returns the
 * string/number/boolean leaf, the sub-object for an interior path, or `null`.
 *
 * @param {string} dotpath
 * @returns {string | number | boolean | object | null}
 */
export function readConfigValue(dotpath) {
  let raw;
  try {
    raw = readFileSync(configJsonPath(), 'utf8');
  } catch {
    return null; // file absent / unreadable → not set
  }
  let parsed;
  try {
    parsed = JSON.parse(raw);
  } catch {
    return null; // corrupt JSON → not set (fail-open)
  }
  let cur = parsed;
  for (const seg of dotpath.split('.')) {
    if (cur === null || typeof cur !== 'object' || !(seg in cur)) return null;
    cur = cur[seg];
  }
  return cur === undefined ? null : cur;
}

/**
 * String-typed convenience: returns a non-empty trimmed string leaf, else null.
 * Trims to match the legacy file-read helpers (which `.trim()` file contents).
 *
 * @param {string} dotpath
 * @returns {string | null}
 */
export function readConfigString(dotpath) {
  const v = readConfigValue(dotpath);
  if (typeof v !== 'string') return null;
  const t = v.trim();
  return t.length > 0 ? t : null;
}
