import { chmodSync } from "node:fs";
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
import { randomUUID } from "node:crypto";
import { config } from "../config.js";
/**
 * True when this entry should be opened as a stdio (subprocess)
 * server, false when remote (URL). Single source of truth for the
 * discriminator — callers don't have to re-derive
 * `cfg.command !== undefined` everywhere, and a future migration to
 * an explicit `kind` field has one place to change.
 */
export function isStdioConfig(cfg) {
    return typeof cfg.command === "string" && cfg.command.length > 0;
}
const SECRET_PLACEHOLDER = "***REDACTED***";
async function ensureDir() {
    await mkdir(dirname(config.mcpConfigFile), { recursive: true });
}
async function atomicWriteJson(data) {
    await ensureDir();
    const path = config.mcpConfigFile;
    const tmp = `${path}.${randomUUID()}.tmp`;
    await writeFile(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
    // Some kernels honour the umask on the initial create; reapply 0600
    // explicitly so the persisted file always matches what we promised
    // in the docstring.
    try {
        chmodSync(tmp, 0o600);
    }
    catch {
        // Best-effort — if chmod fails (e.g. read-only fs in tests), the
        // umask-applied perms are still likely fine.
    }
    try {
        await rename(tmp, path);
    }
    catch (err) {
        await unlink(tmp).catch(() => undefined);
        throw err;
    }
}
export async function readMcpJson() {
    try {
        const raw = await readFile(config.mcpConfigFile, "utf8");
        if (raw.trim().length === 0)
            return { servers: {} };
        const parsed = JSON.parse(raw);
        if (typeof parsed !== "object" || parsed === null || !("servers" in parsed)) {
            return { servers: {} };
        }
        const servers = parsed.servers;
        const disabled = parsed.disabled === true;
        if (typeof servers !== "object" || servers === null) {
            return { disabled, servers: {} };
        }
        return { disabled, servers: servers };
    }
    catch (err) {
        if (err.code === "ENOENT")
            return { servers: {} };
        throw err;
    }
}
/**
 * Copy `src` to a fresh `McpServerConfig` keeping only fields that
 * are not `undefined`. Centralizes the field list so the redact /
 * write paths can't drift from each other when we add a field.
 */
function copyServerCleaned(src) {
    const out = {};
    if (src.enabled !== undefined)
        out.enabled = src.enabled;
    if (src.url !== undefined)
        out.url = src.url;
    if (src.transport !== undefined)
        out.transport = src.transport;
    if (src.command !== undefined)
        out.command = src.command;
    if (src.args !== undefined)
        out.args = [...src.args];
    if (src.cwd !== undefined)
        out.cwd = src.cwd;
    return out;
}
/**
 * Same as readMcpJson but with every secret VALUE replaced with the
 * redaction sentinel. Covers both `headers` (remote) and `env`
 * (stdio) — both fields commonly carry credentials.
 */
export async function readMcpJsonRedacted() {
    const raw = await readMcpJson();
    const out = {};
    for (const [name, server] of Object.entries(raw.servers)) {
        const cleaned = copyServerCleaned(server);
        if (server.headers !== undefined) {
            cleaned.headers = {};
            for (const k of Object.keys(server.headers)) {
                cleaned.headers[k] = SECRET_PLACEHOLDER;
            }
        }
        if (server.env !== undefined) {
            cleaned.env = {};
            for (const k of Object.keys(server.env)) {
                cleaned.env[k] = SECRET_PLACEHOLDER;
            }
        }
        out[name] = cleaned;
    }
    return { disabled: raw.disabled === true, servers: out };
}
/**
 * Merge a sentinel-bearing key/value map with the prior persisted
 * one. Used for both `headers` and `env` — same shape, same logic,
 * single helper. Sentinel ↦ prior value (or drop the key if no
 * prior); real value ↦ overwrite.
 */
function mergeSecretMap(next, prior) {
    const out = {};
    for (const [k, v] of Object.entries(next)) {
        if (v === SECRET_PLACEHOLDER) {
            if (prior?.[k] !== undefined)
                out[k] = prior[k];
        }
        else {
            out[k] = v;
        }
    }
    return out;
}
/**
 * Write `mcp.json`, merging the secret-placeholder for `headers`
 * AND `env` values back to the prior persisted value. Without this,
 * an "edit and save" round-trip from the UI (which sees the
 * sentinel) would write the literal sentinel back to disk and lock
 * the user out of their MCP server. Same pattern as
 * config-manager.writeModelsJson for `apiKey`.
 */
export async function writeMcpJson(next) {
    const existing = await readMcpJson().catch(() => ({ servers: {} }));
    const safe = { servers: {} };
    if (next.disabled === true)
        safe.disabled = true;
    for (const [name, server] of Object.entries(next.servers ?? {})) {
        const merged = copyServerCleaned(server);
        if (server.headers !== undefined) {
            merged.headers = mergeSecretMap(server.headers, existing.servers[name]?.headers);
        }
        if (server.env !== undefined) {
            merged.env = mergeSecretMap(server.env, existing.servers[name]?.env);
        }
        safe.servers[name] = merged;
    }
    await atomicWriteJson(safe);
}
export async function upsertMcpServer(name, server) {
    const cur = await readMcpJson();
    cur.servers[name] = server;
    await writeMcpJson(cur);
}
export async function setMcpDisabled(disabled) {
    const cur = await readMcpJson();
    cur.disabled = disabled;
    await writeMcpJson(cur);
}
export async function deleteMcpServer(name) {
    const cur = await readMcpJson();
    if (cur.servers[name] === undefined)
        return false;
    delete cur.servers[name];
    await writeMcpJson(cur);
    return true;
}
//# sourceMappingURL=config.js.map