/**
 * Configuration schema and loading for MCP proxy and remote server.
 *
 * Config files:
 *   - proxy.config.json  — MCP proxy (local) settings
 *   - remote.config.json — Remote server settings
 *
 * Each loader falls back to a legacy combined config.json (if present)
 * for backward compatibility, then to built-in defaults.
 *
 * Keys directory: ~/.drawlatch/keys/
 */
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { loadConnection } from './connections.js';
/** Resolve the base config directory at call time (not import time).
 *  Defaults to ~/.drawlatch in the user's home directory.
 *  Override with MCP_CONFIG_DIR env var for custom deployments.
 *
 *  These are functions (not constants) so that process.env.MCP_CONFIG_DIR can
 *  be set at runtime before the first call — important for hosts like
 *  callboard that configure the path after ESM imports are resolved. */
export function getConfigDir() {
    return process.env.MCP_CONFIG_DIR ?? path.join(os.homedir(), '.drawlatch');
}
export function getConfigPath() {
    return path.join(getConfigDir(), 'config.json');
}
export function getProxyConfigPath() {
    return path.join(getConfigDir(), 'proxy.config.json');
}
export function getRemoteConfigPath() {
    return path.join(getConfigDir(), 'remote.config.json');
}
export function getKeysDir() {
    return path.join(getConfigDir(), 'keys');
}
export function getCallerKeysDir() {
    return path.join(getKeysDir(), 'callers');
}
export function getServerKeysDir() {
    return path.join(getKeysDir(), 'server');
}
export function getEnvFilePath() {
    return path.join(getConfigDir(), '.env');
}
// ── Defaults ─────────────────────────────────────────────────────────────────
function proxyDefaults() {
    return {
        remoteUrl: 'http://localhost:9999',
        connectTimeout: 10_000,
        requestTimeout: 30_000,
    };
}
function remoteDefaults() {
    return {
        host: '127.0.0.1',
        port: 9999,
        callers: {},
        rateLimitPerMinute: 60,
    };
}
// ── Split config loading (preferred) ─────────────────────────────────────────
/**
 * Load the MCP proxy (local) config.
 *
 * Resolution order:
 *   1. proxy.config.json (flat ProxyConfig)
 *   2. config.json → .proxy section (legacy combined format)
 *   3. Built-in defaults
 *
 * Key paths are derived automatically:
 *   - Caller keys: keys/callers/{MCP_KEY_ALIAS || "default"}/
 *   - Server keys: keys/server/
 */
export function loadProxyConfig() {
    const def = proxyDefaults();
    let config;
    // Try dedicated proxy config file first
    if (fs.existsSync(getProxyConfigPath())) {
        const raw = JSON.parse(fs.readFileSync(getProxyConfigPath(), 'utf-8'));
        config = { ...def, ...raw };
    }
    else if (fs.existsSync(getConfigPath())) {
        // Fall back to combined config.json
        const raw = JSON.parse(fs.readFileSync(getConfigPath(), 'utf-8'));
        config = raw.proxy ? { ...def, ...raw.proxy } : def;
    }
    else {
        config = def;
    }
    return config;
}
/**
 * Resolve the caller key alias for the MCP proxy.
 *
 * Resolution: MCP_KEY_ALIAS env var > "default"
 */
export function resolveCallerKeyAlias() {
    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentionally coerces empty string
    return process.env.MCP_KEY_ALIAS?.trim() || 'default';
}
/**
 * Load the remote server config.
 *
 * Resolution order:
 *   1. remote.config.json (flat RemoteServerConfig)
 *   2. config.json → .remote section (legacy combined format)
 *   3. Built-in defaults
 *
 * Legacy configs with `routes`/`authorizedPeersDir`/`connections` are auto-migrated
 * to the caller-centric format with a deprecation warning.
 */
export function loadRemoteConfig() {
    const def = remoteDefaults();
    let config;
    // Try dedicated remote config file first
    if (fs.existsSync(getRemoteConfigPath())) {
        const raw = JSON.parse(fs.readFileSync(getRemoteConfigPath(), 'utf-8'));
        config = { ...def, ...raw };
    }
    else if (fs.existsSync(getConfigPath())) {
        // Fall back to combined config.json
        const raw = JSON.parse(fs.readFileSync(getConfigPath(), 'utf-8'));
        config = raw.remote ? { ...def, ...raw.remote } : def;
    }
    else {
        config = def;
    }
    // Legacy migration: old format had routes/authorizedPeersDir/connections at top level
    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- reading unknown legacy config shape
    const rawConfig = config;
    if (rawConfig.routes && !('default' in config.callers) && !rawConfig.connectors) {
        console.error('[config] Warning: legacy config format detected (routes/authorizedPeersDir/connections). ' +
            'Migrating to caller-centric format. Please update your remote.config.json.');
        const legacyRoutes = rawConfig.routes;
        const legacyConnections = rawConfig.connections ?? [];
        // Auto-assign aliases to unnamed routes for the default caller
        const connectors = legacyRoutes.map((r, i) => ({
            ...r,
            alias: r.alias ?? r.name?.toLowerCase().replace(/\s+/g, '-') ?? `route-${i}`,
        }));
        const allConnectionNames = [...legacyConnections, ...connectors.map((c) => c.alias)];
        config = {
            ...def,
            host: config.host,
            port: config.port,
            connectors,
            callers: {
                default: {
                    connections: allConnectionNames,
                },
            },
            rateLimitPerMinute: config.rateLimitPerMinute,
        };
    }
    return config;
}
// ── Split config saving ─────────────────────────────────────────────────────
export function saveProxyConfig(config) {
    fs.mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
    fs.writeFileSync(getProxyConfigPath(), JSON.stringify(config, null, 2), { mode: 0o600 });
}
export function saveRemoteConfig(config) {
    fs.mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
    fs.writeFileSync(getRemoteConfigPath(), JSON.stringify(config, null, 2), { mode: 0o600 });
}
// ── Per-caller route resolution ──────────────────────────────────────────
/**
 * Resolve the effective routes for a specific caller.
 *
 * For each connection name in the caller's `connections` list:
 *   1. Check custom connectors (by alias) first
 *   2. Fall back to built-in connection templates (e.g., "github", "stripe")
 *
 * Returns an array of Route objects ready for `resolveRoutes()`.
 */
export function resolveCallerRoutes(config, callerAlias) {
    if (!(callerAlias in config.callers))
        return [];
    const caller = config.callers[callerAlias];
    // Build lookup map for custom connectors by alias
    const connectorsByAlias = new Map();
    for (const c of config.connectors ?? []) {
        if (c.alias)
            connectorsByAlias.set(c.alias, c);
    }
    return caller.connections.map((name) => {
        // Custom connectors take precedence over built-in templates
        const custom = connectorsByAlias.get(name);
        const route = custom ?? loadConnection(name);
        // Ensure every route carries its alias so it survives resolution
        return route.alias === name ? route : { ...route, alias: name };
    });
}
// ── Secret / placeholder resolution ──────────────────────────────────────────
/**
 * Replace ${VAR} placeholders in a string with values from a secrets map.
 * Unknown placeholders are left unchanged (with a warning).
 */
export function resolvePlaceholders(str, secretsMap) {
    return str.replace(/\$\{(\w+)\}/g, (match, name) => {
        if (name in secretsMap)
            return secretsMap[name];
        console.error(`[config] Warning: placeholder ${match} not found in secrets`);
        return match;
    });
}
/**
 * Load secrets from the config's secrets map, resolving from environment
 * variables. Value can be a literal string or "${VAR_NAME}" to read from env.
 *
 * Resolution order for each ${VAR} reference:
 *   1. `envOverrides[VAR]` — caller's explicit env mapping (pre-resolved)
 *   2. `process.env[PREFIX_VAR]` — prefixed env var (e.g., ALICE_GITHUB_TOKEN)
 *   3. Warning if not found (bare process.env fallback intentionally removed
 *      to prevent cross-caller secret leakage)
 *
 * When `callerAlias` is omitted, step 2 is skipped (used for resolving
 * the caller's own env mapping where prefixing doesn't apply).
 */
export function resolveSecrets(secretsMap, envOverrides, callerAlias) {
    const resolved = {};
    for (const [key, value] of Object.entries(secretsMap)) {
        const envMatch = /^\$\{(.+)\}$/.exec(value);
        if (envMatch) {
            const varName = envMatch[1];
            // 1. Caller's explicit env mapping (pre-resolved values)
            let envVal = envOverrides?.[varName];
            // 2. Prefixed env var (e.g., ALICE_GITHUB_TOKEN for caller "alice" + var "GITHUB_TOKEN")
            if (envVal === undefined && callerAlias) {
                const prefix = callerAlias.toUpperCase().replace(/-/g, '_');
                envVal = process.env[`${prefix}_${varName}`];
            }
            if (envVal !== undefined) {
                resolved[key] = envVal;
            }
            else {
                console.error(`[secrets] Warning: env var ${varName} not found for key ${key}${callerAlias ? ` (caller: ${callerAlias})` : ''}`);
            }
        }
        else {
            resolved[key] = value;
        }
    }
    return resolved;
}
/**
 * Resolve all routes: resolve secrets from env vars, then resolve header
 * placeholders against each route's own resolved secrets.
 *
 * When `envOverrides` is provided, those pre-resolved values are checked
 * before process.env during secret resolution (used for per-caller env).
 *
 * When `callerAlias` is provided, prefixed env vars (e.g., ALICE_GITHUB_TOKEN)
 * are checked as a fallback before giving up on a ${VAR} reference.
 */
export function resolveRoutes(routes, envOverrides, callerAlias) {
    return routes.map((route) => {
        const resolvedSecrets = resolveSecrets(route.secrets ?? {}, envOverrides, callerAlias);
        const resolvedHeaders = {};
        for (const [key, value] of Object.entries(route.headers ?? {})) {
            resolvedHeaders[key] = resolvePlaceholders(value, resolvedSecrets);
        }
        return {
            ...(route.alias !== undefined && { alias: route.alias }),
            ...(route.name !== undefined && { name: route.name }),
            ...(route.description !== undefined && { description: route.description }),
            ...(route.docsUrl !== undefined && { docsUrl: route.docsUrl }),
            ...(route.openApiUrl !== undefined && { openApiUrl: route.openApiUrl }),
            ...(route.stability !== undefined && { stability: route.stability }),
            ...(route.category !== undefined && { category: route.category }),
            headers: resolvedHeaders,
            secrets: resolvedSecrets,
            allowedEndpoints: route.allowedEndpoints,
            resolveSecretsInBody: route.resolveSecretsInBody ?? false,
            ...(route.testConnection !== undefined && { testConnection: route.testConnection }),
            ...(route.testIngestor !== undefined && { testIngestor: route.testIngestor }),
            ...(route.listenerConfig !== undefined && { listenerConfig: route.listenerConfig }),
            ...(route.ingestor !== undefined && { ingestorConfig: route.ingestor }),
        };
    });
}
//# sourceMappingURL=config.js.map