import JSON5 from "json5";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { isDeepStrictEqual } from "node:util";
import { loadDotEnv } from "../infra/dotenv.js";
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
import { loadShellEnvFallback, resolveShellEnvFallbackTimeoutMs, shouldDeferShellEnvFallback, shouldEnableShellEnvFallback, } from "../infra/shell-env.js";
import { VERSION } from "../version.js";
import { DuplicateAgentDirError, findDuplicateAgentDirs } from "./agent-dirs.js";
import { applyCompactionDefaults, applyContextPruningDefaults, applyAgentDefaults, applyLoggingDefaults, applyMessageDefaults, applyModelDefaults, applySessionDefaults, applyTalkApiKey, } from "./defaults.js";
import { restoreEnvVarRefs } from "./env-preserve.js";
import { MissingEnvVarError, containsEnvVarReference, resolveConfigEnvVars, } from "./env-substitution.js";
import { collectConfigEnvVars } from "./env-vars.js";
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
import { findLegacyConfigIssues } from "./legacy.js";
import { applyMergePatch } from "./merge-patch.js";
import { normalizeConfigPaths } from "./normalize-paths.js";
import { resolveConfigPath, resolveDefaultConfigCandidates, resolveStateDir } from "./paths.js";
import { applyConfigOverrides } from "./runtime-overrides.js";
import { validateConfigObjectWithPlugins } from "./validation.js";
import { compareClawdbotVersions } from "./version.js";
// Re-export for backwards compatibility
export { CircularIncludeError, ConfigIncludeError } from "./includes.js";
export { MissingEnvVarError } from "./env-substitution.js";
const SHELL_ENV_EXPECTED_KEYS = [
    "OPENAI_API_KEY",
    "ANTHROPIC_API_KEY",
    "ANTHROPIC_OAUTH_TOKEN",
    "GEMINI_API_KEY",
    "ZAI_API_KEY",
    "OPENROUTER_API_KEY",
    "AI_GATEWAY_API_KEY",
    "MINIMAX_API_KEY",
    "SYNTHETIC_API_KEY",
    "ELEVENLABS_API_KEY",
    "TELEGRAM_BOT_TOKEN",
    "DISCORD_BOT_TOKEN",
    "SLACK_BOT_TOKEN",
    "SLACK_APP_TOKEN",
    "OPENCLAW_GATEWAY_TOKEN",
    "OPENCLAW_GATEWAY_PASSWORD",
];
const CONFIG_BACKUP_COUNT = 5;
const loggedInvalidConfigs = new Set();
function hashConfigRaw(raw) {
    return crypto
        .createHash("sha256")
        .update(raw ?? "")
        .digest("hex");
}
export function resolveConfigSnapshotHash(snapshot) {
    if (typeof snapshot.hash === "string") {
        const trimmed = snapshot.hash.trim();
        if (trimmed) {
            return trimmed;
        }
    }
    if (typeof snapshot.raw !== "string") {
        return null;
    }
    return hashConfigRaw(snapshot.raw);
}
function coerceConfig(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return {};
    }
    return value;
}
function isPlainObject(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function cloneUnknown(value) {
    return structuredClone(value);
}
function createMergePatch(base, target) {
    if (!isPlainObject(base) || !isPlainObject(target)) {
        return cloneUnknown(target);
    }
    const patch = {};
    const keys = new Set([...Object.keys(base), ...Object.keys(target)]);
    for (const key of keys) {
        const hasBase = key in base;
        const hasTarget = key in target;
        if (!hasTarget) {
            patch[key] = null;
            continue;
        }
        const targetValue = target[key];
        if (!hasBase) {
            patch[key] = cloneUnknown(targetValue);
            continue;
        }
        const baseValue = base[key];
        if (isPlainObject(baseValue) && isPlainObject(targetValue)) {
            const childPatch = createMergePatch(baseValue, targetValue);
            if (isPlainObject(childPatch) && Object.keys(childPatch).length === 0) {
                continue;
            }
            patch[key] = childPatch;
            continue;
        }
        if (!isDeepStrictEqual(baseValue, targetValue)) {
            patch[key] = cloneUnknown(targetValue);
        }
    }
    return patch;
}
function collectEnvRefPaths(value, path, output) {
    if (typeof value === "string") {
        if (containsEnvVarReference(value)) {
            output.set(path, value);
        }
        return;
    }
    if (Array.isArray(value)) {
        value.forEach((item, index) => {
            collectEnvRefPaths(item, `${path}[${index}]`, output);
        });
        return;
    }
    if (isPlainObject(value)) {
        for (const [key, child] of Object.entries(value)) {
            const childPath = path ? `${path}.${key}` : key;
            collectEnvRefPaths(child, childPath, output);
        }
    }
}
function collectChangedPaths(base, target, path, output) {
    if (Array.isArray(base) && Array.isArray(target)) {
        const max = Math.max(base.length, target.length);
        for (let index = 0; index < max; index += 1) {
            const childPath = path ? `${path}[${index}]` : `[${index}]`;
            if (index >= base.length || index >= target.length) {
                output.add(childPath);
                continue;
            }
            collectChangedPaths(base[index], target[index], childPath, output);
        }
        return;
    }
    if (isPlainObject(base) && isPlainObject(target)) {
        const keys = new Set([...Object.keys(base), ...Object.keys(target)]);
        for (const key of keys) {
            const childPath = path ? `${path}.${key}` : key;
            const hasBase = key in base;
            const hasTarget = key in target;
            if (!hasTarget || !hasBase) {
                output.add(childPath);
                continue;
            }
            collectChangedPaths(base[key], target[key], childPath, output);
        }
        return;
    }
    if (!isDeepStrictEqual(base, target)) {
        output.add(path);
    }
}
function parentPath(value) {
    if (!value) {
        return "";
    }
    if (value.endsWith("]")) {
        const index = value.lastIndexOf("[");
        return index > 0 ? value.slice(0, index) : "";
    }
    const index = value.lastIndexOf(".");
    return index >= 0 ? value.slice(0, index) : "";
}
function isPathChanged(path, changedPaths) {
    if (changedPaths.has(path)) {
        return true;
    }
    let current = parentPath(path);
    while (current) {
        if (changedPaths.has(current)) {
            return true;
        }
        current = parentPath(current);
    }
    return changedPaths.has("");
}
function restoreEnvRefsFromMap(value, path, envRefMap, changedPaths) {
    if (typeof value === "string") {
        if (!isPathChanged(path, changedPaths)) {
            const original = envRefMap.get(path);
            if (original !== undefined) {
                return original;
            }
        }
        return value;
    }
    if (Array.isArray(value)) {
        let changed = false;
        const next = value.map((item, index) => {
            const updated = restoreEnvRefsFromMap(item, `${path}[${index}]`, envRefMap, changedPaths);
            if (updated !== item) {
                changed = true;
            }
            return updated;
        });
        return changed ? next : value;
    }
    if (isPlainObject(value)) {
        let changed = false;
        const next = {};
        for (const [key, child] of Object.entries(value)) {
            const childPath = path ? `${path}.${key}` : key;
            const updated = restoreEnvRefsFromMap(child, childPath, envRefMap, changedPaths);
            if (updated !== child) {
                changed = true;
            }
            next[key] = updated;
        }
        return changed ? next : value;
    }
    return value;
}
async function rotateConfigBackups(configPath, ioFs) {
    if (CONFIG_BACKUP_COUNT <= 1) {
        return;
    }
    const backupBase = `${configPath}.bak`;
    const maxIndex = CONFIG_BACKUP_COUNT - 1;
    await ioFs.unlink(`${backupBase}.${maxIndex}`).catch(() => {
        // best-effort
    });
    for (let index = maxIndex - 1; index >= 1; index -= 1) {
        await ioFs.rename(`${backupBase}.${index}`, `${backupBase}.${index + 1}`).catch(() => {
            // best-effort
        });
    }
    await ioFs.rename(backupBase, `${backupBase}.1`).catch(() => {
        // best-effort
    });
}
function warnOnConfigMiskeys(raw, logger) {
    if (!raw || typeof raw !== "object") {
        return;
    }
    const gateway = raw.gateway;
    if (!gateway || typeof gateway !== "object") {
        return;
    }
    if ("token" in gateway) {
        logger.warn('Config uses "gateway.token". This key is ignored; use "gateway.auth.token" instead.');
    }
}
function stampConfigVersion(cfg) {
    const now = new Date().toISOString();
    return {
        ...cfg,
        meta: {
            ...cfg.meta,
            lastTouchedVersion: VERSION,
            lastTouchedAt: now,
        },
    };
}
function warnIfConfigFromFuture(cfg, logger) {
    const touched = cfg.meta?.lastTouchedVersion;
    if (!touched) {
        return;
    }
    const cmp = compareClawdbotVersions(VERSION, touched);
    if (cmp === null) {
        return;
    }
    if (cmp < 0) {
        logger.warn(`Config was last written by a newer OpenClaw (${touched}); current version is ${VERSION}.`);
    }
}
function applyConfigEnv(cfg, env) {
    const entries = collectConfigEnvVars(cfg);
    for (const [key, value] of Object.entries(entries)) {
        if (env[key]?.trim()) {
            continue;
        }
        env[key] = value;
    }
}
function resolveConfigPathForDeps(deps) {
    if (deps.configPath) {
        return deps.configPath;
    }
    return resolveConfigPath(deps.env, resolveStateDir(deps.env, deps.homedir));
}
function normalizeDeps(overrides = {}) {
    return {
        fs: overrides.fs ?? fs,
        json5: overrides.json5 ?? JSON5,
        env: overrides.env ?? process.env,
        homedir: overrides.homedir ?? (() => resolveRequiredHomeDir(overrides.env ?? process.env, os.homedir)),
        configPath: overrides.configPath ?? "",
        logger: overrides.logger ?? console,
    };
}
function maybeLoadDotEnvForConfig(env) {
    // Only hydrate dotenv for the real process env. Callers using injected env
    // objects (tests/diagnostics) should stay isolated.
    if (env !== process.env) {
        return;
    }
    loadDotEnv({ quiet: true });
}
export function parseConfigJson5(raw, json5 = JSON5) {
    try {
        return { ok: true, parsed: json5.parse(raw) };
    }
    catch (err) {
        return { ok: false, error: String(err) };
    }
}
export function createConfigIO(overrides = {}) {
    const deps = normalizeDeps(overrides);
    const requestedConfigPath = resolveConfigPathForDeps(deps);
    const candidatePaths = deps.configPath
        ? [requestedConfigPath]
        : resolveDefaultConfigCandidates(deps.env, deps.homedir);
    const configPath = candidatePaths.find((candidate) => deps.fs.existsSync(candidate)) ?? requestedConfigPath;
    // Snapshot of env vars captured after applyConfigEnv + resolveConfigEnvVars.
    // Used by writeConfigFile to verify ${VAR} restoration against the env state
    // that produced the resolved config, not the (possibly mutated) live env.
    let envSnapshotForRestore = null;
    function loadConfig() {
        try {
            maybeLoadDotEnvForConfig(deps.env);
            if (!deps.fs.existsSync(configPath)) {
                if (shouldEnableShellEnvFallback(deps.env) && !shouldDeferShellEnvFallback(deps.env)) {
                    loadShellEnvFallback({
                        enabled: true,
                        env: deps.env,
                        expectedKeys: SHELL_ENV_EXPECTED_KEYS,
                        logger: deps.logger,
                        timeoutMs: resolveShellEnvFallbackTimeoutMs(deps.env),
                    });
                }
                return {};
            }
            const raw = deps.fs.readFileSync(configPath, "utf-8");
            const parsed = deps.json5.parse(raw);
            // Resolve $include directives before validation
            const resolved = resolveConfigIncludes(parsed, configPath, {
                readFile: (p) => deps.fs.readFileSync(p, "utf-8"),
                parseJson: (raw) => deps.json5.parse(raw),
            });
            // Apply config.env to process.env BEFORE substitution so ${VAR} can reference config-defined vars
            if (resolved && typeof resolved === "object" && "env" in resolved) {
                applyConfigEnv(resolved, deps.env);
            }
            // Substitute ${VAR} env var references
            const substituted = resolveConfigEnvVars(resolved, deps.env);
            // Capture env snapshot after substitution for use by writeConfigFile.
            // This ensures restoreEnvVarRefs verifies against the env that produced
            // the resolved values, not a potentially mutated live env (TOCTOU fix).
            envSnapshotForRestore = { ...deps.env };
            const resolvedConfig = substituted;
            warnOnConfigMiskeys(resolvedConfig, deps.logger);
            if (typeof resolvedConfig !== "object" || resolvedConfig === null) {
                return {};
            }
            const preValidationDuplicates = findDuplicateAgentDirs(resolvedConfig, {
                env: deps.env,
                homedir: deps.homedir,
            });
            if (preValidationDuplicates.length > 0) {
                throw new DuplicateAgentDirError(preValidationDuplicates);
            }
            const validated = validateConfigObjectWithPlugins(resolvedConfig);
            if (!validated.ok) {
                const details = validated.issues
                    .map((iss) => `- ${iss.path || "<root>"}: ${iss.message}`)
                    .join("\n");
                if (!loggedInvalidConfigs.has(configPath)) {
                    loggedInvalidConfigs.add(configPath);
                    deps.logger.error(`Invalid config at ${configPath}:\\n${details}`);
                }
                const error = new Error("Invalid config");
                error.code = "INVALID_CONFIG";
                error.details = details;
                throw error;
            }
            if (validated.warnings.length > 0) {
                const details = validated.warnings
                    .map((iss) => `- ${iss.path || "<root>"}: ${iss.message}`)
                    .join("\n");
                deps.logger.warn(`Config warnings:\\n${details}`);
            }
            warnIfConfigFromFuture(validated.config, deps.logger);
            const cfg = applyModelDefaults(applyCompactionDefaults(applyContextPruningDefaults(applyAgentDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config)))))));
            normalizeConfigPaths(cfg);
            const duplicates = findDuplicateAgentDirs(cfg, {
                env: deps.env,
                homedir: deps.homedir,
            });
            if (duplicates.length > 0) {
                throw new DuplicateAgentDirError(duplicates);
            }
            applyConfigEnv(cfg, deps.env);
            const enabled = shouldEnableShellEnvFallback(deps.env) || cfg.env?.shellEnv?.enabled === true;
            if (enabled && !shouldDeferShellEnvFallback(deps.env)) {
                loadShellEnvFallback({
                    enabled: true,
                    env: deps.env,
                    expectedKeys: SHELL_ENV_EXPECTED_KEYS,
                    logger: deps.logger,
                    timeoutMs: cfg.env?.shellEnv?.timeoutMs ?? resolveShellEnvFallbackTimeoutMs(deps.env),
                });
            }
            return applyConfigOverrides(cfg);
        }
        catch (err) {
            if (err instanceof DuplicateAgentDirError) {
                deps.logger.error(err.message);
                throw err;
            }
            const error = err;
            if (error?.code === "INVALID_CONFIG") {
                return {};
            }
            deps.logger.error(`Failed to read config at ${configPath}`, err);
            return {};
        }
    }
    async function readConfigFileSnapshot() {
        maybeLoadDotEnvForConfig(deps.env);
        const exists = deps.fs.existsSync(configPath);
        if (!exists) {
            const hash = hashConfigRaw(null);
            const config = applyTalkApiKey(applyModelDefaults(applyCompactionDefaults(applyContextPruningDefaults(applyAgentDefaults(applySessionDefaults(applyMessageDefaults({})))))));
            const legacyIssues = [];
            return {
                path: configPath,
                exists: false,
                raw: null,
                parsed: {},
                // @ts-ignore -- cherry-pick upstream type mismatch
                resolved: {},
                valid: true,
                // @ts-ignore -- cherry-pick upstream type mismatch
                // @ts-ignore -- cherry-pick upstream type mismatch
                config,
                hash,
                issues: [],
                warnings: [],
                legacyIssues,
            };
        }
        try {
            const raw = deps.fs.readFileSync(configPath, "utf-8");
            const hash = hashConfigRaw(raw);
            const parsedRes = parseConfigJson5(raw, deps.json5);
            if (!parsedRes.ok) {
                return {
                    path: configPath,
                    exists: true,
                    raw,
                    parsed: {},
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    resolved: {},
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    valid: false,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    config: {},
                    hash,
                    issues: [{ path: "", message: `JSON5 parse failed: ${parsedRes.error}` }],
                    warnings: [],
                    legacyIssues: [],
                };
            }
            // Resolve $include directives
            let resolved;
            try {
                resolved = resolveConfigIncludes(parsedRes.parsed, configPath, {
                    readFile: (p) => deps.fs.readFileSync(p, "utf-8"),
                    parseJson: (raw) => deps.json5.parse(raw),
                });
            }
            catch (err) {
                const message = err instanceof ConfigIncludeError
                    ? err.message
                    : `Include resolution failed: ${String(err)}`;
                return {
                    path: configPath,
                    exists: true,
                    raw,
                    parsed: parsedRes.parsed,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    resolved: coerceConfig(parsedRes.parsed),
                    valid: false,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    config: coerceConfig(parsedRes.parsed),
                    hash,
                    issues: [{ path: "", message }],
                    warnings: [],
                    legacyIssues: [],
                };
            }
            // Apply config.env to process.env BEFORE substitution so ${VAR} can reference config-defined vars
            if (resolved && typeof resolved === "object" && "env" in resolved) {
                applyConfigEnv(resolved, deps.env);
            }
            // Substitute ${VAR} env var references
            let substituted;
            try {
                substituted = resolveConfigEnvVars(resolved, deps.env);
                // Capture env snapshot (same as loadConfig — see TOCTOU comment above)
                envSnapshotForRestore = { ...deps.env };
            }
            catch (err) {
                const message = err instanceof MissingEnvVarError
                    ? err.message
                    : `Env var substitution failed: ${String(err)}`;
                return {
                    path: configPath,
                    exists: true,
                    raw,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    parsed: parsedRes.parsed,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    resolved: coerceConfig(resolved),
                    valid: false,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    config: coerceConfig(resolved),
                    hash,
                    issues: [{ path: "", message }],
                    warnings: [],
                    legacyIssues: [],
                };
            }
            const resolvedConfigRaw = substituted;
            const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw);
            const validated = validateConfigObjectWithPlugins(resolvedConfigRaw);
            if (!validated.ok) {
                return {
                    path: configPath,
                    exists: true,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    raw,
                    parsed: parsedRes.parsed,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    resolved: coerceConfig(resolvedConfigRaw),
                    valid: false,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    config: coerceConfig(resolvedConfigRaw),
                    hash,
                    issues: validated.issues,
                    warnings: validated.warnings,
                    legacyIssues,
                };
            }
            warnIfConfigFromFuture(validated.config, deps.logger);
            return {
                path: configPath,
                exists: true,
                raw,
                // @ts-ignore -- cherry-pick upstream type mismatch
                parsed: parsedRes.parsed,
                // Use resolvedConfigRaw (after $include and ${ENV} substitution but BEFORE runtime defaults)
                // for config set/unset operations (issue #6070)
                // @ts-ignore -- cherry-pick upstream type mismatch
                resolved: coerceConfig(resolvedConfigRaw),
                valid: true,
                // @ts-ignore -- cherry-pick upstream type mismatch
                config: normalizeConfigPaths(applyTalkApiKey(applyModelDefaults(applyAgentDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config))))))),
                hash,
                issues: [],
                warnings: validated.warnings,
                legacyIssues,
            };
        }
        catch (err) {
            return {
                // @ts-ignore -- cherry-pick upstream type mismatch
                path: configPath,
                exists: true,
                raw: null,
                parsed: {},
                // @ts-ignore -- cherry-pick upstream type mismatch
                resolved: {},
                valid: false,
                // @ts-ignore -- cherry-pick upstream type mismatch
                config: {},
                hash: hashConfigRaw(null),
                issues: [{ path: "", message: `read failed: ${String(err)}` }],
                warnings: [],
                legacyIssues: [],
            };
        }
    }
    async function writeConfigFile(cfg) {
        clearConfigCache();
        let persistCandidate = cfg;
        // Save the injected env snapshot before readConfigFileSnapshot() overwrites it
        // with a new snapshot based on the (possibly mutated) live env.
        const savedEnvSnapshot = envSnapshotForRestore;
        const snapshot = await readConfigFileSnapshot();
        let envRefMap = null;
        let changedPaths = null;
        // @ts-ignore -- cherry-pick upstream type mismatch
        if (savedEnvSnapshot) {
            envSnapshotForRestore = savedEnvSnapshot;
        }
        if (snapshot.valid && snapshot.exists) {
            const patch = createMergePatch(snapshot.config, cfg);
            // @ts-ignore -- cherry-pick upstream type mismatch
            persistCandidate = applyMergePatch(snapshot.resolved, patch);
            try {
                // @ts-ignore -- cherry-pick upstream type mismatch
                const resolvedIncludes = resolveConfigIncludes(snapshot.parsed, configPath, {
                    readFile: (candidate) => deps.fs.readFileSync(candidate, "utf-8"),
                    parseJson: (raw) => deps.json5.parse(raw),
                });
                const collected = new Map();
                collectEnvRefPaths(resolvedIncludes, "", collected);
                if (collected.size > 0) {
                    envRefMap = collected;
                    changedPaths = new Set();
                    collectChangedPaths(snapshot.config, cfg, "", changedPaths);
                }
            }
            catch {
                envRefMap = null;
            }
        }
        const validated = validateConfigObjectWithPlugins(persistCandidate);
        if (!validated.ok) {
            const issue = validated.issues[0];
            const pathLabel = issue?.path ? issue.path : "<root>";
            throw new Error(`Config validation failed: ${pathLabel}: ${issue?.message ?? "invalid"}`);
        }
        if (validated.warnings.length > 0) {
            const details = validated.warnings
                .map((warning) => `- ${warning.path}: ${warning.message}`)
                .join("\n");
            deps.logger.warn(`Config warnings:\n${details}`);
        }
        // Restore ${VAR} env var references that were resolved during config loading.
        // Read the current file (pre-substitution) and restore any references whose
        // resolved values match the incoming config — so we don't overwrite
        // "${ANTHROPIC_API_KEY}" with "sk-ant-..." when the caller didn't change it.
        //
        // We use only the root file's parsed content (no $include resolution) to avoid
        // pulling values from included files into the root config on write-back.
        // Apply env restoration to validated.config (which has runtime defaults stripped
        // per issue #6070) rather than the raw caller input.
        let cfgToWrite = validated.config;
        try {
            if (deps.fs.existsSync(configPath)) {
                const currentRaw = await deps.fs.promises.readFile(configPath, "utf-8");
                const parsedRes = parseConfigJson5(currentRaw, deps.json5);
                if (parsedRes.ok) {
                    // Use env snapshot from when config was loaded (if available) to avoid
                    // TOCTOU issues where env changes between load and write. Falls back to
                    // live env if no snapshot exists (e.g., first write before any load).
                    const envForRestore = envSnapshotForRestore ?? deps.env;
                    cfgToWrite = restoreEnvVarRefs(cfgToWrite, parsedRes.parsed, envForRestore);
                }
            }
        }
        catch {
            // If reading the current file fails, write cfg as-is (no env restoration)
        }
        const dir = path.dirname(configPath);
        await deps.fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
        const outputConfig = envRefMap && changedPaths
            ? restoreEnvRefsFromMap(cfgToWrite, "", envRefMap, changedPaths)
            : cfgToWrite;
        // Do NOT apply runtime defaults when writing — user config should only contain
        // explicitly set values. Runtime defaults are applied when loading (issue #6070).
        const json = JSON.stringify(stampConfigVersion(outputConfig), null, 2).trimEnd().concat("\n");
        const nextHash = hashConfigRaw(json);
        const previousHash = resolveConfigSnapshotHash(snapshot);
        const changedPathCount = changedPaths?.size;
        const logConfigOverwrite = () => {
            if (!snapshot.exists) {
                return;
            }
            const changeSummary = typeof changedPathCount === "number" ? `, changedPaths=${changedPathCount}` : "";
            deps.logger.warn(`Config overwrite: ${configPath} (sha256 ${previousHash ?? "unknown"} -> ${nextHash}, backup=${configPath}.bak${changeSummary})`);
        };
        const tmp = path.join(dir, `${path.basename(configPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
        await deps.fs.promises.writeFile(tmp, json, {
            encoding: "utf-8",
            mode: 0o600,
        });
        if (deps.fs.existsSync(configPath)) {
            await rotateConfigBackups(configPath, deps.fs.promises);
            await deps.fs.promises.copyFile(configPath, `${configPath}.bak`).catch(() => {
                // best-effort
            });
        }
        try {
            await deps.fs.promises.rename(tmp, configPath);
        }
        catch (err) {
            const code = err.code;
            // Windows doesn't reliably support atomic replace via rename when dest exists.
            if (code === "EPERM" || code === "EEXIST") {
                await deps.fs.promises.copyFile(tmp, configPath);
                await deps.fs.promises.chmod(configPath, 0o600).catch(() => {
                    // best-effort
                });
                await deps.fs.promises.unlink(tmp).catch(() => {
                    // best-effort
                });
                logConfigOverwrite();
                return;
            }
            await deps.fs.promises.unlink(tmp).catch(() => {
                // best-effort
            });
            throw err;
        }
        logConfigOverwrite();
    }
    return {
        configPath,
        loadConfig,
        readConfigFileSnapshot,
        writeConfigFile,
        /** Return the env snapshot captured during the last loadConfig/readConfigFileSnapshot, or null. */
        getEnvSnapshot() {
            return envSnapshotForRestore;
        },
        /** Inject an env snapshot (e.g. from a prior IO instance) for use by writeConfigFile. */
        setEnvSnapshot(snapshot) {
            envSnapshotForRestore = snapshot;
        },
    };
}
// NOTE: These wrappers intentionally do *not* cache the resolved config path at
// module scope. `OPENCLAW_CONFIG_PATH` (and friends) are expected to work even
// when set after the module has been imported (tests, one-off scripts, etc.).
const DEFAULT_CONFIG_CACHE_MS = 200;
let configCache = null;
function resolveConfigCacheMs(env) {
    const raw = env.OPENCLAW_CONFIG_CACHE_MS?.trim();
    if (raw === "" || raw === "0") {
        return 0;
    }
    if (!raw) {
        return DEFAULT_CONFIG_CACHE_MS;
    }
    const parsed = Number.parseInt(raw, 10);
    if (!Number.isFinite(parsed)) {
        return DEFAULT_CONFIG_CACHE_MS;
    }
    return Math.max(0, parsed);
}
function shouldUseConfigCache(env) {
    if (env.OPENCLAW_DISABLE_CONFIG_CACHE?.trim()) {
        return false;
    }
    return resolveConfigCacheMs(env) > 0;
}
export function clearConfigCache() {
    configCache = null;
}
export function loadConfig() {
    const io = createConfigIO();
    const configPath = io.configPath;
    const now = Date.now();
    if (shouldUseConfigCache(process.env)) {
        const cached = configCache;
        if (cached && cached.configPath === configPath && cached.expiresAt > now) {
            return cached.config;
        }
    }
    const config = io.loadConfig();
    if (shouldUseConfigCache(process.env)) {
        const cacheMs = resolveConfigCacheMs(process.env);
        if (cacheMs > 0) {
            configCache = {
                configPath,
                expiresAt: now + cacheMs,
                config,
            };
        }
    }
    return config;
}
export async function readConfigFileSnapshot() {
    return await createConfigIO().readConfigFileSnapshot();
}
export async function readConfigFileSnapshotForWrite() {
    const io = createConfigIO();
    const snapshot = await io.readConfigFileSnapshot();
    return {
        snapshot,
        writeOptions: {
            envSnapshotForRestore: io.getEnvSnapshot() ?? undefined,
            expectedConfigPath: io.configPath,
        },
    };
}
export async function writeConfigFile(cfg, options = {}) {
    clearConfigCache();
    const io = createConfigIO();
    const sameConfigPath = options.expectedConfigPath === undefined || options.expectedConfigPath === io.configPath;
    if (sameConfigPath && options.envSnapshotForRestore) {
        io.setEnvSnapshot(options.envSnapshotForRestore);
    }
    await io.writeConfigFile(cfg);
}
