/**
 * k8s-secret-render.ts — render `REPLACE_ME_FROM_*` placeholders in the
 * shipped k8s Secret templates with real values, and persist the rendered
 * set so reapply is idempotent.
 *
 * B5 of olam-issue-npm-only-k3s (npm-only operator bootstrap completion).
 *
 * The templates at `packages/host-cp/k8s/templates/*-secret-template.yaml`
 * carry placeholders like `REPLACE_ME_FROM_HOME_DOTOLAM_AUTH_SECRET`. The
 * source-of-truth value for each placeholder is documented in the template
 * comment block; it's either a host-side file in `~/.olam/` (the compose
 * bootstrap path) or a subprocess output (e.g. `gh auth token`).
 *
 * Source resolution per placeholder:
 *
 *   placeholder                                              → source
 *   REPLACE_ME_FROM_HOME_DOTOLAM_AUTH_SECRET                  → file ~/.olam/auth-secret           (generate if missing)
 *   REPLACE_ME_FROM_GH_AUTH_TOKEN                             → subprocess `gh auth token`         (warn + skip if absent)
 *   REPLACE_ME_FROM_HOME_DOTOLAM_AUTH_DB_SECRET               → file ~/.olam/auth-db-secret        (generate if missing)
 *   REPLACE_ME_FROM_HOME_DOTOLAM_MCP_AUTH_JWT_SECRET          → file ~/.olam/mcp-auth-jwt-secret   (generate if missing)
 *   REPLACE_ME_FROM_HOME_DOTOLAM_KG_BEARER_TOKEN              → file ~/.olam/kg-bearer-token       (generate if missing)
 *   REPLACE_ME_FROM_HOME_DOTOLAM_MEMORY_BEARER_SECRET         → file ~/.olam/memory-bearer-secret  (generate if missing)
 *
 * State persistence: `~/.olam/k8s-secrets-state.json` records the rendered
 * Secret set so re-runs (idempotent reapply) reuse the same values. Without
 * the state file every `olam upgrade` would rotate tokens silently, breaking
 * worlds that have cached the previous value. Operator opts in to rotation
 * via `--rotate-secrets`.
 *
 * D20 stdin-safe design: rendered YAML is built in-process and applied via
 * stdin to `kubectl apply -f -`. Placeholder values are NEVER inlined into
 * subprocess argv (audit:auth-callers + general security hygiene).
 */
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, statSync, renameSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { randomBytes } from 'node:crypto';
import { OLAM_HOME } from './config.js';
export const K8S_SECRETS_STATE_PATH = join(OLAM_HOME, 'k8s-secrets-state.json');
const SECRET_HEX_BYTES = 32; // 64-char hex tokens — mirrors memory-secret.ts SECRET_LEN_BYTES.
/** Canonical mapping for the 5 secret templates that ship with the CLI. */
export const SECRET_TEMPLATE_BINDINGS = [
    {
        secretName: 'olam-host-cp-secret',
        templateRelPath: 'templates/40-secret-template.yaml',
        placeholders: [
            {
                placeholder: 'REPLACE_ME_FROM_HOME_DOTOLAM_AUTH_SECRET',
                key: 'OLAM_AUTH_SECRET',
                source: { kind: 'file', hostFile: 'auth-secret' },
            },
            {
                placeholder: 'REPLACE_ME_FROM_GH_AUTH_TOKEN',
                key: 'GH_TOKEN',
                source: { kind: 'gh-token' },
            },
        ],
    },
    {
        secretName: 'olam-auth-service-secret',
        templateRelPath: 'templates/auth-service-secret-template.yaml',
        placeholders: [
            {
                placeholder: 'REPLACE_ME_FROM_HOME_DOTOLAM_AUTH_DB_SECRET',
                key: 'OLAM_AUTH_DB_SECRET',
                source: { kind: 'file', hostFile: 'auth-db-secret' },
            },
        ],
    },
    {
        secretName: 'olam-mcp-auth-service-secret',
        templateRelPath: 'templates/mcp-auth-service-secret-template.yaml',
        placeholders: [
            {
                placeholder: 'REPLACE_ME_FROM_HOME_DOTOLAM_MCP_AUTH_JWT_SECRET',
                key: 'OLAM_MCP_AUTH_JWT_SECRET',
                source: { kind: 'file', hostFile: 'mcp-auth-jwt-secret' },
            },
        ],
    },
    {
        secretName: 'olam-kg-service-secret',
        templateRelPath: 'templates/kg-service-secret-template.yaml',
        placeholders: [
            {
                placeholder: 'REPLACE_ME_FROM_HOME_DOTOLAM_KG_BEARER_TOKEN',
                key: 'OLAM_KG_BEARER_TOKEN',
                source: { kind: 'file', hostFile: 'kg-bearer-token' },
            },
        ],
    },
    {
        secretName: 'olam-memory-service-secret',
        templateRelPath: 'templates/memory-service-secret-template.yaml',
        placeholders: [
            {
                placeholder: 'REPLACE_ME_FROM_HOME_DOTOLAM_MEMORY_BEARER_SECRET',
                key: 'OLAM_MEMORY_BEARER_SECRET',
                source: { kind: 'file', hostFile: 'memory-bearer-secret' },
            },
        ],
    },
];
const defaultGenRandomHex = () => randomBytes(SECRET_HEX_BYTES).toString('hex');
const defaultRunGhTokenCmd = () => {
    const r = spawnSync('gh', ['auth', 'token'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
    if (r.status === 0 && r.stdout.trim().length > 0) {
        return { ok: true, token: r.stdout.trim() };
    }
    return { ok: false, token: '' };
};
const defaultReadFile = (p, enc) => readFileSync(p, enc);
const defaultWriteFile = (p, data, mode) => {
    mkdirSync(dirname(p), { recursive: true });
    writeFileSync(p, data, { encoding: 'utf8', mode });
    // writeFileSync's mode is umask-respecting; chmod explicitly to be safe.
    chmodSync(p, mode);
};
const defaultFileExists = (p) => existsSync(p);
/**
 * Read the state file. Returns null when missing OR when the version doesn't match (forward-compat).
 */
export function readSecretsState(statePath = K8S_SECRETS_STATE_PATH) {
    if (!existsSync(statePath))
        return null;
    try {
        const raw = readFileSync(statePath, 'utf8');
        const parsed = JSON.parse(raw);
        if (parsed.version !== 1)
            return null;
        return parsed;
    }
    catch {
        return null;
    }
}
/** Atomically write the state file (0600). */
export function writeSecretsState(state, statePath = K8S_SECRETS_STATE_PATH) {
    mkdirSync(dirname(statePath), { recursive: true });
    const tmp = `${statePath}.tmp.${process.pid}`;
    writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
    chmodSync(tmp, 0o600);
    // renameSync is atomic on POSIX. Use the statically-imported binding; a
    // dynamic require('node:fs') would survive type-checking but esbuild's
    // ESM bundler emits it as `__require("node:fs")` which throws at runtime
    // ("Dynamic require of \"node:fs\" is not supported"), killing
    // `olam upgrade` mid-bootstrap. See R4-W2-B retest finding.
    renameSync(tmp, statePath);
}
/**
 * Resolve a single placeholder's value. Returns null when the source is
 * absent AND not generatable (gh-token only). For file-sourced placeholders
 * we always generate if missing — that's the npm-only operator path.
 */
function resolveSourceValue(source, olamHome, reuseFromState, rotate, deps) {
    if (!rotate && reuseFromState !== undefined && reuseFromState !== '') {
        return { ok: true, value: reuseFromState };
    }
    if (source.kind === 'file') {
        const filePath = join(olamHome, source.hostFile);
        // Rotation: always generate fresh value, overwrite the host file.
        if (rotate) {
            const fresh = deps.genRandomHex();
            deps.writeFile(filePath, fresh, 0o600);
            return { ok: true, value: fresh };
        }
        if (deps.fileExists(filePath)) {
            const value = deps.readFile(filePath, 'utf8').trim();
            if (value.length > 0)
                return { ok: true, value };
        }
        // Missing or empty — generate, persist, return.
        const fresh = deps.genRandomHex();
        deps.writeFile(filePath, fresh, 0o600);
        return { ok: true, value: fresh };
    }
    // kind === 'gh-token' — not generatable; warn + skip if absent.
    // Rotation reads gh CLI again (gh manages its own session rotation).
    const r = deps.runGhTokenCmd();
    if (r.ok)
        return { ok: true, value: r.token };
    return { ok: false, reason: 'gh CLI not authenticated; run `gh auth login` then re-run' };
}
/**
 * Substitute the placeholder strings in the template YAML with real values.
 * Builds a fresh Secret YAML string (does NOT touch the template on disk).
 *
 * The templates hold ONE placeholder per stringData key; literal string
 * substitution suffices. We avoid YAML parsing to keep the template comment
 * block (which carries operator-facing documentation) intact in the rendered
 * output.
 */
export function substitutePlaceholders(templateYaml, values) {
    let out = templateYaml;
    for (const [placeholder, value] of Object.entries(values)) {
        // Use split/join rather than .replace to avoid regex escaping pitfalls.
        out = out.split(`"${placeholder}"`).join(`"${value}"`);
    }
    return out;
}
/**
 * Render ONE secret template. Reads the template YAML from `templatesRoot`
 * (resolved by the caller to either source or install-mode location),
 * resolves each placeholder, persists generated values to host files,
 * substitutes, and returns the rendered YAML.
 *
 * `reuse` carries the prior state's per-key values when an unrotated
 * re-apply is requested; null means "this is the first apply or operator
 * asked for rotation."
 */
export function renderOneSecret(binding, templatesRoot, reuse, deps = {}, rotate = false) {
    const olamHome = deps.olamHome ?? OLAM_HOME;
    const readFile = deps.readFile ?? defaultReadFile;
    const writeFile = deps.writeFile ?? defaultWriteFile;
    const fileExists = deps.fileExists ?? defaultFileExists;
    const genRandomHex = deps.genRandomHex ?? defaultGenRandomHex;
    const runGhTokenCmd = deps.runGhTokenCmd ?? defaultRunGhTokenCmd;
    const templatePath = join(templatesRoot, binding.templateRelPath);
    const templateYaml = readFile(templatePath, 'utf8');
    const keys = {};
    const placeholderToValue = {};
    const missingSources = [];
    for (const p of binding.placeholders) {
        const resolved = resolveSourceValue(p.source, olamHome, reuse?.keys[p.key], rotate, { readFile, writeFile, fileExists, genRandomHex, runGhTokenCmd });
        if (!resolved.ok) {
            missingSources.push(`${p.key}: ${resolved.reason}`);
            continue;
        }
        keys[p.key] = resolved.value;
        placeholderToValue[p.placeholder] = resolved.value;
    }
    if (missingSources.length > 0) {
        return {
            secretName: binding.secretName,
            status: 'skipped',
            keys,
            missingSources,
        };
    }
    const renderedYaml = substitutePlaceholders(templateYaml, placeholderToValue);
    return {
        secretName: binding.secretName,
        status: 'resolved',
        renderedYaml,
        keys,
        missingSources: [],
    };
}
/**
 * Render the full set of shipped Secret templates. `rotate=true` discards
 * the on-disk state and regenerates every value; `rotate=false` reuses
 * state for unchanged secrets so worlds caching old tokens don't break.
 */
export function renderAllSecrets(templatesRoot, opts, deps = {}) {
    const readState = deps.readState ?? (() => readSecretsState(deps.statePath ?? K8S_SECRETS_STATE_PATH));
    const writeState = deps.writeState ?? ((s) => writeSecretsState(s, deps.statePath ?? K8S_SECRETS_STATE_PATH));
    const prior = opts.rotate ? null : readState();
    const results = [];
    const nextSecrets = {};
    for (const binding of SECRET_TEMPLATE_BINDINGS) {
        const reuse = prior?.secrets[binding.secretName] ?? null;
        const r = renderOneSecret(binding, templatesRoot, reuse, deps, opts.rotate);
        results.push(r);
        nextSecrets[binding.secretName] = {
            keys: r.keys,
            ...(r.missingSources.length > 0 ? { skipped: r.missingSources } : {}),
        };
    }
    const nextState = {
        version: 1,
        context: opts.context,
        namespace: opts.namespace,
        generatedAt: new Date().toISOString(),
        secrets: nextSecrets,
    };
    // Only persist when at least one secret resolved — avoids an empty state file
    // on a wholly-failed first run (where every gh-token source was absent and
    // the operator hadn't yet generated `~/.olam/*` files).
    if (results.some((r) => r.status === 'resolved')) {
        writeState(nextState);
    }
    return { results, nextState };
}
//# sourceMappingURL=k8s-secret-render.js.map