/**
 * Sentinel value used to replace sensitive config fields in gateway responses.
 * Write-side handlers (config.set, config.apply, config.patch) detect this
 * sentinel and restore the original value from the on-disk config, so a
 * round-trip through the Web UI does not corrupt credentials.
 */
export const REDACTED_SENTINEL = "__OPENCLAW_REDACTED__";
/**
 * Non-sensitive field names that happen to match sensitive patterns.
 * These are explicitly excluded from redaction.
 */
const SENSITIVE_KEY_WHITELIST = new Set([
    "maxtokens",
    "maxoutputtokens",
    "maxinputtokens",
    "maxcompletiontokens",
    "contexttokens",
    "totaltokens",
    "tokencount",
    "tokenlimit",
    "tokenbudget",
]);
/**
 * Patterns that identify sensitive config field names.
 * Aligned with the UI-hint logic in schema.ts.
 */
const SENSITIVE_KEY_PATTERNS = [/token$/i, /password/i, /secret/i, /api.?key/i];
function isSensitiveKey(key) {
    if (SENSITIVE_KEY_WHITELIST.has(key.toLowerCase())) {
        return false;
    }
    return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key));
}
/**
 * Deep-walk an object and replace values whose key matches a sensitive pattern
 * with the redaction sentinel.
 */
function redactObject(obj) {
    if (obj === null || obj === undefined) {
        return obj;
    }
    if (typeof obj !== "object") {
        return obj;
    }
    if (Array.isArray(obj)) {
        return obj.map(redactObject);
    }
    const result = {};
    for (const [key, value] of Object.entries(obj)) {
        if (isSensitiveKey(key) && value !== null && value !== undefined) {
            result[key] = REDACTED_SENTINEL;
        }
        else if (typeof value === "object" && value !== null) {
            result[key] = redactObject(value);
        }
        else {
            result[key] = value;
        }
    }
    return result;
}
export function redactConfigObject(value) {
    return redactObject(value);
}
/**
 * Collect all sensitive string values from a config object.
 * Used for text-based redaction of the raw JSON5 source.
 */
function collectSensitiveValues(obj) {
    const values = [];
    if (obj === null || obj === undefined || typeof obj !== "object") {
        return values;
    }
    if (Array.isArray(obj)) {
        for (const item of obj) {
            values.push(...collectSensitiveValues(item));
        }
        return values;
    }
    for (const [key, value] of Object.entries(obj)) {
        if (isSensitiveKey(key) && typeof value === "string" && value.length > 0) {
            values.push(value);
        }
        else if (typeof value === "object" && value !== null) {
            values.push(...collectSensitiveValues(value));
        }
    }
    return values;
}
/**
 * Replace known sensitive values in a raw JSON5 string with the sentinel.
 * Values are replaced longest-first to avoid partial matches.
 */
function redactRawText(raw, config) {
    const sensitiveValues = collectSensitiveValues(config);
    sensitiveValues.sort((a, b) => b.length - a.length);
    let result = raw;
    for (const value of sensitiveValues) {
        const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
        result = result.replace(new RegExp(escaped, "g"), REDACTED_SENTINEL);
    }
    const keyValuePattern = /(^|[{\s,])((["'])([^"']+)\3|([A-Za-z0-9_$.-]+))(\s*:\s*)(["'])([^"']*)\7/g;
    result = result.replace(keyValuePattern, (match, prefix, keyExpr, _keyQuote, keyQuoted, keyBare, sep, valQuote, val) => {
        const key = (keyQuoted ?? keyBare);
        if (!key || !isSensitiveKey(key)) {
            return match;
        }
        if (val === REDACTED_SENTINEL) {
            return match;
        }
        return `${prefix}${keyExpr}${sep}${valQuote}${REDACTED_SENTINEL}${valQuote}`;
    });
    return result;
}
/**
 * Returns a copy of the config snapshot with all sensitive fields
 * replaced by {@link REDACTED_SENTINEL}. The `hash` is preserved
 * (it tracks config identity, not content).
 *
 * Both `config` (the parsed object) and `raw` (the JSON5 source) are scrubbed
 * so no credential can leak through either path.
 */
export function redactConfigSnapshot(snapshot) {
    const redactedConfig = redactConfigObject(snapshot.config);
    const redactedRaw = snapshot.raw ? redactRawText(snapshot.raw, snapshot.config) : null;
    const redactedParsed = snapshot.parsed ? redactConfigObject(snapshot.parsed) : snapshot.parsed;
    return {
        ...snapshot,
        config: redactedConfig,
        raw: redactedRaw,
        parsed: redactedParsed,
    };
}
/**
 * Deep-walk `incoming` and replace any {@link REDACTED_SENTINEL} values
 * (on sensitive keys) with the corresponding value from `original`.
 *
 * This is called by config.set / config.apply / config.patch before writing,
 * so that credentials survive a Web UI round-trip unmodified.
 */
export function restoreRedactedValues(incoming, original) {
    if (incoming === null || incoming === undefined) {
        return incoming;
    }
    if (typeof incoming !== "object") {
        return incoming;
    }
    if (Array.isArray(incoming)) {
        const origArr = Array.isArray(original) ? original : [];
        return incoming.map((item, i) => restoreRedactedValues(item, origArr[i]));
    }
    const orig = original && typeof original === "object" && !Array.isArray(original)
        ? original
        : {};
    const result = {};
    for (const [key, value] of Object.entries(incoming)) {
        if (isSensitiveKey(key) && value === REDACTED_SENTINEL) {
            if (!(key in orig)) {
                throw new Error(`config write rejected: "${key}" is redacted; set an explicit value instead of ${REDACTED_SENTINEL}`);
            }
            result[key] = orig[key];
        }
        else if (typeof value === "object" && value !== null) {
            result[key] = restoreRedactedValues(value, orig[key]);
        }
        else {
            result[key] = value;
        }
    }
    return result;
}
