import { getModels, getProviders } from "@earendil-works/pi-ai/compat";
import { existsSync, readFileSync } from "fs";
import { normalizeContextWindowOptions, validateContextWindowValue } from "./context-window.js";
import { mergeCompat, mergeCustomModels } from "./model-registry-builtins.js";
import { formatValidationPath, stripJsonComments, validateModelsConfig, } from "./model-registry-schemas.js";
function modelRequestKey(provider, modelId) {
    return `${provider}:${modelId}`;
}
function emptyCustomModelsResult(error) {
    return {
        models: [],
        overrides: new Map(),
        modelOverrides: new Map(),
        providerRequestConfigs: new Map(),
        modelRequestHeaders: new Map(),
        modelRequestHeaderSources: new Map(),
        error,
    };
}
function mergeModelOverrides(base, incoming) {
    const merged = new Map([...base].map(([providerName, overrides]) => [providerName, new Map(overrides)]));
    for (const [providerName, incomingOverrides] of incoming) {
        const providerOverrides = new Map(merged.get(providerName) ?? []);
        for (const [modelId, modelOverride] of incomingOverrides) {
            providerOverrides.set(modelId, modelOverride);
        }
        merged.set(providerName, providerOverrides);
    }
    return merged;
}
function mergeModelRequestHeaders(base, incoming) {
    const headers = new Map(base.modelRequestHeaders);
    const sources = new Map(base.modelRequestHeaderSources);
    const invalidate = (key, source) => {
        if (sources.get(key) !== source)
            return;
        headers.delete(key);
        sources.delete(key);
    };
    for (const model of incoming.models) {
        invalidate(modelRequestKey(model.provider, model.id), "model");
    }
    for (const [providerName, overrides] of incoming.modelOverrides) {
        for (const modelId of overrides.keys()) {
            invalidate(modelRequestKey(providerName, modelId), "modelOverride");
        }
    }
    for (const [key, value] of incoming.modelRequestHeaders)
        headers.set(key, value);
    for (const [key, source] of incoming.modelRequestHeaderSources)
        sources.set(key, source);
    return { headers, sources };
}
function mergeCustomModelResults(base, incoming) {
    const mergedHeaders = mergeModelRequestHeaders(base, incoming);
    return {
        models: mergeCustomModels(base.models, incoming.models),
        overrides: new Map([...base.overrides, ...incoming.overrides]),
        modelOverrides: mergeModelOverrides(base.modelOverrides, incoming.modelOverrides),
        providerRequestConfigs: new Map([...base.providerRequestConfigs, ...incoming.providerRequestConfigs]),
        modelRequestHeaders: mergedHeaders.headers,
        modelRequestHeaderSources: mergedHeaders.sources,
        error: undefined,
    };
}
function collectProviderRequestConfig(providerName, config, requestConfigs) {
    if (!config.apiKey && !config.headers && !config.authHeader)
        return;
    requestConfigs.set(providerName, {
        apiKey: config.apiKey,
        headers: config.headers,
        authHeader: config.authHeader,
    });
}
function collectModelHeaders(providerName, modelId, headers, modelHeaders, modelHeaderSources, source) {
    if (!headers || Object.keys(headers).length === 0)
        return;
    const key = modelRequestKey(providerName, modelId);
    modelHeaders.set(key, headers);
    modelHeaderSources.set(key, source);
}
function validateContextWindowOptions(providerName, modelId, options) {
    for (const option of options ?? []) {
        if (validateContextWindowValue(option)) {
            throw new Error(`Provider ${providerName}, model ${modelId}: invalid contextWindowOptions value`);
        }
    }
}
function validateConfig(config) {
    const builtInProviders = new Set(getProviders());
    for (const [providerName, providerConfig] of Object.entries(config.providers)) {
        const isBuiltIn = builtInProviders.has(providerName);
        const hasProviderApi = !!providerConfig.api;
        const models = providerConfig.models ?? [];
        const hasModelOverrides = providerConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0;
        if (models.length === 0) {
            if (!providerConfig.baseUrl && !providerConfig.headers && !providerConfig.compat && !hasModelOverrides) {
                throw new Error(`Provider ${providerName}: must specify "baseUrl", "headers", "compat", "modelOverrides", or "models".`);
            }
        }
        else if (!isBuiltIn) {
            if (!providerConfig.baseUrl) {
                throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`);
            }
            if (!providerConfig.apiKey) {
                throw new Error(`Provider ${providerName}: "apiKey" is required when defining custom models.`);
            }
        }
        for (const modelDef of models) {
            const hasModelApi = !!modelDef.api;
            if (!hasProviderApi && !hasModelApi && !isBuiltIn) {
                throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`);
            }
            if (!modelDef.id)
                throw new Error(`Provider ${providerName}: model missing "id"`);
            if (modelDef.contextWindow !== undefined && validateContextWindowValue(modelDef.contextWindow)) {
                throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
            }
            validateContextWindowOptions(providerName, modelDef.id, modelDef.contextWindowOptions);
            if (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0) {
                throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
            }
        }
        for (const [modelId, modelOverride] of Object.entries(providerConfig.modelOverrides ?? {})) {
            if (modelOverride.contextWindow !== undefined && validateContextWindowValue(modelOverride.contextWindow)) {
                throw new Error(`Provider ${providerName}, model ${modelId}: invalid contextWindow`);
            }
            validateContextWindowOptions(providerName, modelId, modelOverride.contextWindowOptions);
            if (modelOverride.maxTokens !== undefined && modelOverride.maxTokens <= 0) {
                throw new Error(`Provider ${providerName}, model ${modelId}: invalid maxTokens`);
            }
        }
    }
}
function parseModels(config, modelHeaders, modelHeaderSources) {
    const models = [];
    const builtInProviders = new Set(getProviders());
    const builtInDefaultsCache = new Map();
    const getBuiltInDefaults = (providerName) => {
        if (!builtInProviders.has(providerName))
            return undefined;
        if (builtInDefaultsCache.has(providerName))
            return builtInDefaultsCache.get(providerName);
        const builtIn = getModels(providerName);
        if (builtIn.length === 0)
            return undefined;
        const defaults = { api: builtIn[0].api, baseUrl: builtIn[0].baseUrl };
        builtInDefaultsCache.set(providerName, defaults);
        return defaults;
    };
    for (const [providerName, providerConfig] of Object.entries(config.providers)) {
        const modelDefs = providerConfig.models ?? [];
        if (modelDefs.length === 0)
            continue;
        const builtInDefaults = getBuiltInDefaults(providerName);
        for (const modelDef of modelDefs) {
            const api = modelDef.api ?? providerConfig.api ?? builtInDefaults?.api;
            if (!api)
                continue;
            const baseUrl = modelDef.baseUrl ?? providerConfig.baseUrl ?? builtInDefaults?.baseUrl;
            if (!baseUrl)
                continue;
            const compat = mergeCompat(providerConfig.compat, modelDef.compat);
            collectModelHeaders(providerName, modelDef.id, modelDef.headers, modelHeaders, modelHeaderSources, "model");
            const defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
            const contextWindow = modelDef.contextWindow ?? 128000;
            models.push({
                id: modelDef.id,
                name: modelDef.name ?? modelDef.id,
                api: api,
                provider: providerName,
                baseUrl,
                reasoning: modelDef.reasoning ?? false,
                thinkingLevelMap: modelDef.thinkingLevelMap,
                input: (modelDef.input ?? ["text"]),
                cost: modelDef.cost ?? defaultCost,
                contextWindow,
                defaultContextWindow: contextWindow,
                contextWindowOptions: normalizeContextWindowOptions([contextWindow, ...(modelDef.contextWindowOptions ?? [])]),
                maxTokens: modelDef.maxTokens ?? 16384,
                headers: undefined,
                compat,
            });
        }
    }
    return models;
}
function loadCustomModels(modelsJsonPath) {
    if (!existsSync(modelsJsonPath)) {
        return emptyCustomModelsResult();
    }
    try {
        const content = readFileSync(modelsJsonPath, "utf-8");
        const parsed = JSON.parse(stripJsonComments(content));
        if (!validateModelsConfig.Check(parsed)) {
            const errors = validateModelsConfig
                .Errors(parsed)
                .map((error) => `  - ${formatValidationPath(error)}: ${error.message}`)
                .join("\n") || "Unknown schema error";
            return emptyCustomModelsResult(`Invalid models.json schema:\n${errors}\n\nFile: ${modelsJsonPath}`);
        }
        const config = parsed;
        validateConfig(config);
        const overrides = new Map();
        const modelOverrides = new Map();
        const providerRequestConfigs = new Map();
        const modelRequestHeaders = new Map();
        const modelRequestHeaderSources = new Map();
        for (const [providerName, providerConfig] of Object.entries(config.providers)) {
            if (providerConfig.baseUrl || providerConfig.compat) {
                overrides.set(providerName, {
                    baseUrl: providerConfig.baseUrl,
                    compat: providerConfig.compat,
                });
            }
            collectProviderRequestConfig(providerName, providerConfig, providerRequestConfigs);
            if (providerConfig.modelOverrides) {
                modelOverrides.set(providerName, new Map(Object.entries(providerConfig.modelOverrides)));
                for (const [modelId, modelOverride] of Object.entries(providerConfig.modelOverrides)) {
                    collectModelHeaders(providerName, modelId, modelOverride.headers, modelRequestHeaders, modelRequestHeaderSources, "modelOverride");
                }
            }
        }
        return {
            models: parseModels(config, modelRequestHeaders, modelRequestHeaderSources),
            overrides,
            modelOverrides,
            providerRequestConfigs,
            modelRequestHeaders,
            modelRequestHeaderSources,
            error: undefined,
        };
    }
    catch (error) {
        if (error instanceof SyntaxError) {
            return emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\n\nFile: ${modelsJsonPath}`);
        }
        return emptyCustomModelsResult(`Failed to load models.json: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsJsonPath}`);
    }
}
export function loadCustomModelsFromPaths(modelsJsonPaths) {
    let combined = emptyCustomModelsResult();
    const errors = [];
    for (let i = modelsJsonPaths.length - 1; i >= 0; i--) {
        const result = loadCustomModels(modelsJsonPaths[i]);
        if (result.error) {
            errors.push(result.error);
            continue;
        }
        combined = mergeCustomModelResults(combined, result);
    }
    return { ...combined, error: errors.length > 0 ? errors.join("\n\n") : undefined };
}
//# sourceMappingURL=model-registry-custom-loader.js.map