/**
 * YAML/JSON proxy configuration loader with environment variable resolution.
 *
 * Supports:
 * - Loading config from YAML or JSON files
 * - Environment variable interpolation: ${VAR_NAME} and ${VAR_NAME:-default}
 * - Multi-account proxy configurations
 * - Defaults for optional fields
 *
 * YAML parsing uses `js-yaml` when available (dynamic import), otherwise
 * falls back to JSON.parse.
 */
import { readFile } from "node:fs/promises";
import { extname } from "node:path";
import { logger } from "../utils/logger.js";
// ---------------------------------------------------------------------------
// Environment variable resolution
// ---------------------------------------------------------------------------
/**
 * Regex matching `${VAR}` and `${VAR:-default}` patterns.
 * Non-greedy to handle multiple vars on a single line.
 */
const ENV_VAR_PATTERN = /\$\{([^}:]+?)(?::-(.*?))?\}/g;
/**
 * Replace all `${VAR}` / `${VAR:-default}` references in a string.
 *
 * Resolution order:
 * 1. Look up `VAR` in the provided env map (or process.env)
 * 2. If not found, use the `:-default` value when present
 * 3. If no default, leave the original `${VAR}` token so callers can detect
 *    unresolved variables.
 */
export function resolveEnvVars(value, env = process.env) {
    return value.replace(ENV_VAR_PATTERN, (_match, varName, defaultValue) => {
        const envValue = env[varName];
        if (envValue !== undefined) {
            return envValue;
        }
        if (defaultValue !== undefined) {
            return defaultValue;
        }
        // Leave unresolved — callers can detect this
        return `\${${varName}}`;
    });
}
/**
 * Recursively walk an object tree and resolve env vars in every string value.
 */
function resolveEnvVarsDeep(obj, env) {
    if (typeof obj === "string") {
        return resolveEnvVars(obj, env);
    }
    if (Array.isArray(obj)) {
        return obj.map((item) => resolveEnvVarsDeep(item, env));
    }
    if (obj !== null && typeof obj === "object") {
        const result = {};
        for (const [key, val] of Object.entries(obj)) {
            result[key] = resolveEnvVarsDeep(val, env);
        }
        return result;
    }
    return obj;
}
/**
 * Walk the resolved config tree and warn about any remaining `${VAR}`
 * placeholders that were not resolved.  Returns the list of unresolved
 * variable names so callers can decide whether to abort.
 */
function warnUnresolvedPlaceholders(obj, path = "") {
    const unresolved = [];
    if (typeof obj === "string") {
        // Reset global regex state before matching
        ENV_VAR_PATTERN.lastIndex = 0;
        let match = ENV_VAR_PATTERN.exec(obj);
        while (match !== null) {
            const varName = match[1];
            unresolved.push(varName);
            logger.warn(`Unresolved placeholder \${${varName}} at "${path}" — ` +
                "check that the environment variable is set or provide a default with ${VAR:-default}");
            match = ENV_VAR_PATTERN.exec(obj);
        }
    }
    else if (Array.isArray(obj)) {
        for (let i = 0; i < obj.length; i++) {
            unresolved.push(...warnUnresolvedPlaceholders(obj[i], `${path}[${i}]`));
        }
    }
    else if (obj !== null && typeof obj === "object") {
        for (const [key, val] of Object.entries(obj)) {
            unresolved.push(...warnUnresolvedPlaceholders(val, path ? `${path}.${key}` : key));
        }
    }
    return unresolved;
}
/**
 * Check for unresolved `${VAR}` placeholders in critical account fields
 * (apiKey, token, key) and throw if any are found. Non-critical unresolved
 * placeholders are allowed (they only produce warnings).
 */
function failOnUnresolvedAccountCredentials(obj) {
    if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
        return;
    }
    const raw = obj;
    const accounts = raw.accounts;
    if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) {
        return;
    }
    const criticalFields = ["apiKey", "token", "key"];
    const failures = [];
    for (const [provider, list] of Object.entries(accounts)) {
        if (!Array.isArray(list)) {
            continue;
        }
        for (let i = 0; i < list.length; i++) {
            const acct = list[i];
            if (!acct || typeof acct !== "object") {
                continue;
            }
            for (const field of criticalFields) {
                const val = acct[field];
                if (typeof val === "string") {
                    ENV_VAR_PATTERN.lastIndex = 0;
                    if (ENV_VAR_PATTERN.test(val)) {
                        ENV_VAR_PATTERN.lastIndex = 0;
                        failures.push(`accounts.${provider}[${i}].${field}`);
                    }
                    ENV_VAR_PATTERN.lastIndex = 0;
                }
            }
        }
    }
    if (failures.length > 0) {
        throw new Error(`Unresolved environment variable placeholders in critical account fields:\n  - ${failures.join("\n  - ")}\n` +
            "Set the required environment variables or provide defaults with ${VAR:-default}.");
    }
}
// ---------------------------------------------------------------------------
// YAML parsing (dynamic import with fallback)
// ---------------------------------------------------------------------------
/** Shape of the dynamically-imported `js-yaml` module. */
/**
 * Parse YAML content into a JS object.
 * Uses `js-yaml` if available (dynamic import), otherwise falls back to
 * JSON.parse.
 */
async function parseYaml(content) {
    let yaml;
    try {
        yaml = (await import(/* @vite-ignore */ "js-yaml"));
    }
    catch {
        // js-yaml not installed — try JSON fallback
        logger.debug("[ProxyConfig] js-yaml not available, falling back to JSON parser");
        try {
            return JSON.parse(content);
        }
        catch {
            throw new Error("Failed to parse proxy config: js-yaml is not installed and the file is not valid JSON");
        }
    }
    // js-yaml is available — parse YAML (let syntax errors propagate)
    try {
        return yaml.default?.load?.(content) ?? yaml.load(content);
    }
    catch (err) {
        throw new Error(`Failed to parse proxy config as YAML: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
    }
}
// ---------------------------------------------------------------------------
// Defaults & validation
// ---------------------------------------------------------------------------
const DEFAULT_WEIGHT = 1;
const DEFAULT_ENABLED = true;
/**
 * Apply default values to an account config.
 */
function applyAccountDefaults(account) {
    return {
        name: account.name ?? "unnamed",
        apiKey: account.apiKey ?? "",
        baseUrl: account.baseUrl,
        orgId: account.orgId,
        weight: account.weight ?? DEFAULT_WEIGHT,
        enabled: account.enabled ?? DEFAULT_ENABLED,
        rateLimit: account.rateLimit,
        metadata: account.metadata,
    };
}
/**
 * Validate the shape of a parsed proxy config.
 * Returns an array of human-readable error strings (empty = valid).
 */
export function validateProxyConfig(config) {
    const errors = [];
    if (!config || typeof config !== "object") {
        errors.push("Config must be a non-null object");
        return errors;
    }
    const cfg = config;
    if (cfg.version !== undefined && typeof cfg.version !== "number") {
        errors.push(`"version" must be a number, got ${typeof cfg.version}`);
    }
    const hasAccounts = !!cfg.accounts &&
        typeof cfg.accounts === "object" &&
        !Array.isArray(cfg.accounts);
    const hasRouting = !!cfg.routing &&
        typeof cfg.routing === "object" &&
        !Array.isArray(cfg.routing);
    if (cfg.routing !== undefined && !hasRouting) {
        errors.push('"routing" must be an object');
        return errors;
    }
    if (!hasAccounts && !hasRouting) {
        errors.push('Config must contain at least one of "accounts" or "routing"');
        return errors;
    }
    if (cfg.accounts !== undefined && !hasAccounts) {
        errors.push('"accounts" must be an object mapping provider names to account arrays');
        return errors;
    }
    if (hasAccounts) {
        const accounts = cfg.accounts;
        let totalAccounts = 0;
        for (const [provider, list] of Object.entries(accounts)) {
            if (!Array.isArray(list)) {
                errors.push(`accounts.${provider} must be an array, got ${typeof list}`);
                continue;
            }
            totalAccounts += list.length;
            for (let i = 0; i < list.length; i++) {
                const acct = list[i];
                if (!acct || typeof acct !== "object") {
                    errors.push(`accounts.${provider}[${i}] must be an object`);
                    continue;
                }
                if (typeof acct.apiKey !== "string" || acct.apiKey.length === 0) {
                    errors.push(`accounts.${provider}[${i}].apiKey is required and must be a non-empty string`);
                }
            }
        }
        if (totalAccounts === 0 && !hasRouting) {
            errors.push('"accounts" must contain at least one account');
        }
    }
    return errors;
}
// ---------------------------------------------------------------------------
// Plaintext key detection
// ---------------------------------------------------------------------------
/**
 * Detect API keys stored as plaintext (not using `${ENV_VAR}` references)
 * and log a warning for each account.
 */
function warnPlaintextApiKeys(accounts) {
    for (const [provider, list] of Object.entries(accounts)) {
        for (const acct of list) {
            if (acct.apiKey &&
                acct.apiKey.length > 0 &&
                !ENV_VAR_PATTERN.test(acct.apiKey)) {
                // Reset the regex lastIndex (it has the global flag)
                ENV_VAR_PATTERN.lastIndex = 0;
                logger.warn(`\u26A0 API key stored in plaintext in config file for ${provider}/${acct.name}. ` +
                    "Consider using ${ENV_VAR} references.");
            }
            // Also reset after a non-match test
            ENV_VAR_PATTERN.lastIndex = 0;
        }
    }
}
// ---------------------------------------------------------------------------
// Routing config parser
// ---------------------------------------------------------------------------
/**
 * Parse the optional `routing` section from a raw proxy config object.
 *
 * Extracts:
 * - `strategy` ("round-robin" | "fill-first")
 * - `model-mappings` / `modelMappings` — array of {from, to, provider}
 * - `fallback-chain` / `fallbackChain` — array of {provider, model}
 * - `passthroughModels` / `passthrough-models` — array of model IDs
 *
 * Accepts both camelCase and kebab-case keys for YAML-friendliness.
 */
function parseRoutingConfig(raw) {
    if (!raw || typeof raw !== "object") {
        return undefined;
    }
    const result = {};
    // Strategy
    const strategy = raw.strategy;
    if (strategy === "round-robin" || strategy === "fill-first") {
        result.strategy = strategy;
    }
    // Model mappings (accept kebab-case or camelCase)
    const rawMappings = (raw["model-mappings"] ?? raw.modelMappings);
    if (Array.isArray(rawMappings)) {
        result.modelMappings = rawMappings
            .filter((m) => m !== null && typeof m === "object")
            .map((m) => {
            const from = String(m.from ?? "").trim();
            const to = String(m.to ?? "").trim();
            const provider = String(m.provider ?? "anthropic").trim() || "anthropic";
            if (!from || !to) {
                logger.warn(`[proxy-config] Skipping model mapping with empty "from" or "to": ${JSON.stringify(m)}`);
                return null;
            }
            return {
                from,
                to,
                provider,
            };
        })
            .filter((m) => m !== null);
    }
    // Fallback chain (accept kebab-case or camelCase)
    const rawFallback = (raw["fallback-chain"] ?? raw.fallbackChain);
    if (Array.isArray(rawFallback)) {
        result.fallbackChain = rawFallback
            .filter((e) => e !== null && typeof e === "object")
            .map((e) => {
            const provider = String(e.provider ?? "").trim();
            const model = String(e.model ?? "").trim();
            if (!provider || !model) {
                logger.warn(`[proxy-config] Skipping fallback entry with empty "provider" or "model": ${JSON.stringify(e)}`);
                return null;
            }
            return { provider, model };
        })
            .filter((e) => e !== null);
    }
    // Passthrough models (accept kebab-case or camelCase)
    const rawPassthrough = (raw["passthrough-models"] ??
        raw.passthroughModels);
    if (Array.isArray(rawPassthrough)) {
        result.passthroughModels = rawPassthrough.map(String);
    }
    return result;
}
// ---------------------------------------------------------------------------
// Cloaking config validation
// ---------------------------------------------------------------------------
const VALID_CLOAKING_MODES = new Set(["auto", "always", "never"]);
/**
 * Validate and return a CloakingConfig, or `undefined` if the section is absent.
 * Throws on structurally invalid input so problems surface at config-load time
 * rather than at first proxy request.
 */
function validateCloakingConfig(raw) {
    if (raw === undefined || raw === null) {
        return undefined;
    }
    if (typeof raw !== "object" || Array.isArray(raw)) {
        throw new Error(`Invalid proxy config: "cloaking" must be an object, got ${typeof raw}`);
    }
    const obj = raw;
    if (!VALID_CLOAKING_MODES.has(obj.mode)) {
        throw new Error(`Invalid proxy config: "cloaking.mode" must be one of "auto", "always", "never", got "${String(obj.mode)}"`);
    }
    if (obj.plugins !== undefined &&
        (typeof obj.plugins !== "object" ||
            obj.plugins === null ||
            Array.isArray(obj.plugins))) {
        throw new Error(`Invalid proxy config: "cloaking.plugins" must be an object, got ${typeof obj.plugins}`);
    }
    return raw;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Load and parse a proxy configuration file (YAML or JSON).
 *
 * @param filePath - Absolute or relative path to the config file.
 * @param options  - Optional settings for env resolution.
 * @returns Parsed and validated ProxyConfigFile.
 * @throws When the file cannot be read, parsed, or fails validation.
 */
export async function loadProxyConfig(filePath, options = {}) {
    const { resolveEnv: shouldResolve = true, env = process.env } = options;
    logger.debug("[ProxyConfig] Loading proxy config", { filePath });
    // 1. Read file
    let content;
    try {
        content = await readFile(filePath, "utf-8");
    }
    catch (err) {
        throw new Error(`Failed to read proxy config file: ${filePath} — ${err.message}`, { cause: err });
    }
    // 2. Parse (YAML for .yml/.yaml, JSON for .json, try YAML-then-JSON for others)
    const ext = extname(filePath).toLowerCase();
    let parsed;
    if (ext === ".json") {
        try {
            parsed = JSON.parse(content);
        }
        catch (err) {
            throw new Error(`Failed to parse JSON proxy config: ${err.message}`, { cause: err });
        }
    }
    else {
        // .yml, .yaml, or unknown — try YAML (which also handles JSON)
        parsed = await parseYaml(content);
    }
    // 3. Warn about plaintext API keys BEFORE env-var resolution so that
    //    correctly parameterized `${ENV_VAR}` configs are not false-positived.
    //    Guard each entry: only process arrays (non-arrays are caught by
    //    validateProxyConfig below, so we must not crash here first).
    {
        const preResolvedRaw = parsed;
        const preResolvedAccounts = preResolvedRaw.accounts;
        if (preResolvedAccounts &&
            typeof preResolvedAccounts === "object" &&
            !Array.isArray(preResolvedAccounts)) {
            const preAccounts = {};
            for (const [provider, list] of Object.entries(preResolvedAccounts)) {
                if (!Array.isArray(list)) {
                    continue;
                }
                preAccounts[provider] = list.map((item) => applyAccountDefaults(item));
            }
            warnPlaintextApiKeys(preAccounts);
        }
    }
    // 4. Resolve env vars
    if (shouldResolve) {
        parsed = resolveEnvVarsDeep(parsed, env);
        // 4b. Warn about any placeholders that could not be resolved
        warnUnresolvedPlaceholders(parsed);
        // 4c. Fail hard if critical credential fields still have unresolved vars
        failOnUnresolvedAccountCredentials(parsed);
    }
    // 5. Validate
    const errors = validateProxyConfig(parsed);
    if (errors.length > 0) {
        throw new Error(`Invalid proxy config:\n  - ${errors.join("\n  - ")}`);
    }
    // 6. Apply defaults
    const raw = parsed;
    const accounts = {};
    const rawAccounts = raw.accounts;
    if (rawAccounts &&
        typeof rawAccounts === "object" &&
        !Array.isArray(rawAccounts)) {
        for (const [provider, list] of Object.entries(rawAccounts)) {
            accounts[provider] = list.map((item) => applyAccountDefaults(item));
        }
    }
    // 7. Extract routing config
    const routing = parseRoutingConfig(raw.routing);
    // 8. Extract and validate cloaking config
    const cloaking = validateCloakingConfig(raw.cloaking);
    const result = {
        version: raw.version ?? 1,
        defaultProvider: raw.defaultProvider,
        defaultBaseUrl: raw.defaultBaseUrl,
        accounts,
        routing,
        cloaking,
    };
    logger.debug("[ProxyConfig] Proxy config loaded successfully", {
        providers: Object.keys(accounts),
        totalAccounts: Object.values(accounts).reduce((sum, a) => sum + a.length, 0),
        hasRouting: !!routing,
        hasCloaking: !!cloaking,
    });
    return result;
}
/**
 * Load proxy config from a raw string (YAML or JSON) instead of a file path.
 * Useful for testing or when config is stored in environment variables.
 */
export async function parseProxyConfigString(content, options = {}) {
    const { resolveEnv: shouldResolve = true, env = process.env } = options;
    let parsed = await parseYaml(content);
    // Warn about plaintext API keys BEFORE env-var resolution (same as loadProxyConfig).
    {
        const preResolvedRaw = parsed;
        const preResolvedAccounts = preResolvedRaw.accounts;
        if (preResolvedAccounts &&
            typeof preResolvedAccounts === "object" &&
            !Array.isArray(preResolvedAccounts)) {
            const preAccounts = {};
            for (const [provider, list] of Object.entries(preResolvedAccounts)) {
                if (!Array.isArray(list)) {
                    continue;
                }
                preAccounts[provider] = list.map((item) => applyAccountDefaults(item));
            }
            warnPlaintextApiKeys(preAccounts);
        }
    }
    if (shouldResolve) {
        parsed = resolveEnvVarsDeep(parsed, env);
        // Warn about any placeholders that could not be resolved
        warnUnresolvedPlaceholders(parsed);
        // Fail hard if critical credential fields still have unresolved vars
        failOnUnresolvedAccountCredentials(parsed);
    }
    const errors = validateProxyConfig(parsed);
    if (errors.length > 0) {
        throw new Error(`Invalid proxy config:\n  - ${errors.join("\n  - ")}`);
    }
    const raw = parsed;
    const accounts = {};
    const rawAccounts = raw.accounts;
    if (rawAccounts &&
        typeof rawAccounts === "object" &&
        !Array.isArray(rawAccounts)) {
        for (const [provider, list] of Object.entries(rawAccounts)) {
            accounts[provider] = list.map((item) => applyAccountDefaults(item));
        }
    }
    const routing = parseRoutingConfig(raw.routing);
    const cloaking = validateCloakingConfig(raw.cloaking);
    return {
        version: raw.version ?? 1,
        defaultProvider: raw.defaultProvider,
        defaultBaseUrl: raw.defaultBaseUrl,
        accounts,
        routing,
        cloaking,
    };
}
//# sourceMappingURL=proxyConfig.js.map