/**
 * config.ts — `~/.olam/config.json` helper for olam-cli.
 *
 * Phase 1b A1 of olam-host-suite-phase-1b-k3s-beta-flavour (plan
 * ~/.claude/plans/olam-host-suite-phase-1b-k3s-beta-flavour.md).
 *
 * Schema v1 (Decision 1 — config is the substrate-state seam):
 *   {
 *     "config.schema": 1,
 *     "host": { "substrate": "compose" | "kubernetes" },
 *     "install_id": "<uuid-v4>"
 *   }
 *
 * - `install_id` is generated once on first config write; persists thereafter;
 *   never rotated. Used by instrumentation events (D21) to enable
 *   distinct-operator counting at month-6 retro.
 * - Graceful fallback to `{config.schema: 1, host: {substrate: 'compose'}, install_id}`
 *   if file missing or invalid (T1 — never crash).
 * - `~/.olam/state/` directory ensured to exist (recursive mkdir) before any
 *   state-file writes elsewhere in olam-cli (Phase A1 closes feasibility F3-03
 *   from pass-3 review).
 */
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import { randomUUID } from 'node:crypto';
export const OLAM_HOME = join(homedir(), '.olam');
export const OLAM_STATE_DIR = join(OLAM_HOME, 'state');
export const OLAM_CONFIG_PATH = join(OLAM_HOME, 'config.json');
// k3s mode is the canonical olam substrate as of feat/cli-bootstrap-k3s-default
// (2026-05-22). Fresh installs default to `kubernetes`; existing installs whose
// ~/.olam/config.json explicitly carries `host.substrate: 'compose'` keep their
// current behaviour (compose mode remains supported but is no longer the
// default). To pin compose on a fresh install: `olam config set host.substrate compose`.
const DEFAULT_SUBSTRATE = 'kubernetes';
function makeDefaultConfig() {
    return {
        'config.schema': 1,
        host: { substrate: DEFAULT_SUBSTRATE },
        install_id: randomUUID(),
    };
}
function ensureStateDir(stateDir = OLAM_STATE_DIR) {
    if (!existsSync(stateDir)) {
        mkdirSync(stateDir, { recursive: true });
    }
}
function isValidConfig(value) {
    if (typeof value !== 'object' || value === null)
        return false;
    const v = value;
    if (v['config.schema'] !== 1)
        return false;
    if (typeof v.host !== 'object' || v.host === null)
        return false;
    const host = v.host;
    if (host.substrate !== 'compose' && host.substrate !== 'kubernetes')
        return false;
    if (typeof v.install_id !== 'string' || v.install_id.length === 0)
        return false;
    return true;
}
function atomicWriteJSON(path, value, stderr = process.stderr) {
    ensureStateDir();
    const dir = dirname(path);
    if (!existsSync(dir)) {
        mkdirSync(dir, { recursive: true });
    }
    const tmp = `${path}.tmp.${process.pid}`;
    try {
        writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', { encoding: 'utf8' });
        renameSync(tmp, path);
    }
    catch (err) {
        if (existsSync(tmp)) {
            try {
                unlinkSync(tmp);
            }
            catch {
                // best-effort cleanup; surface original error to caller
            }
        }
        stderr.write(`[WARN] olam config write failed: ${err instanceof Error ? err.message : String(err)}\n`);
        throw err;
    }
}
/**
 * Read `~/.olam/config.json` (or `configPath` override). Returns a valid
 * OlamConfig regardless of file state:
 *   - File missing → write default + return.
 *   - File present + valid → return parsed.
 *   - File present + malformed → emit WARN to stderr; write default + return.
 *
 * Always ensures `~/.olam/state/` exists as a side-effect.
 *
 * @param opts.configPath — override for tests (default: `~/.olam/config.json`)
 * @param opts.stateDir — override for tests (default: `~/.olam/state`)
 * @param opts.stderr — override for tests (default: `process.stderr`)
 */
export function readConfig(opts = {}) {
    const configPath = opts.configPath ?? OLAM_CONFIG_PATH;
    const stateDir = opts.stateDir ?? OLAM_STATE_DIR;
    const stderr = opts.stderr ?? process.stderr;
    ensureStateDir(stateDir);
    if (!existsSync(configPath)) {
        const fresh = makeDefaultConfig();
        atomicWriteJSON(configPath, fresh, stderr);
        return fresh;
    }
    let parsed;
    try {
        const raw = readFileSync(configPath, 'utf8');
        parsed = JSON.parse(raw);
    }
    catch (err) {
        stderr.write(`[WARN] olam config malformed at ${configPath}: ${err instanceof Error ? err.message : String(err)}; falling back to defaults\n`);
        const fresh = makeDefaultConfig();
        atomicWriteJSON(configPath, fresh, stderr);
        return fresh;
    }
    if (!isValidConfig(parsed)) {
        stderr.write(`[WARN] olam config schema invalid at ${configPath}; falling back to defaults\n`);
        const fresh = makeDefaultConfig();
        atomicWriteJSON(configPath, fresh, stderr);
        return fresh;
    }
    return parsed;
}
/**
 * Apply a partial update to the config and persist atomically.
 *
 * Preserves `install_id` (never rotated by writeConfig). Caller MUST NOT
 * pass `install_id` in updates; if they do, the existing value wins.
 *
 * @param updates — partial config (only `host` is settable via this surface)
 * @param opts — same as readConfig for test injection
 * @returns the new OlamConfig that was persisted
 */
export function writeConfig(updates, opts = {}) {
    const configPath = opts.configPath ?? OLAM_CONFIG_PATH;
    const stderr = opts.stderr ?? process.stderr;
    const current = readConfig(opts);
    const next = {
        'config.schema': 1,
        host: { ...current.host, ...(updates.host ?? {}) },
        install_id: current.install_id,
    };
    atomicWriteJSON(configPath, next, stderr);
    return next;
}
//# sourceMappingURL=config.js.map