import { getModels, getProviders } from "@earendil-works/pi-ai";
import { z } from "zod";
import { TAU_PROVIDER_EXTENSIONS } from "./tau_extensions.js";
const CostSchema = z
    .object({
    input: z.number().nonnegative().optional(),
    output: z.number().nonnegative().optional(),
    cacheRead: z.number().nonnegative().optional(),
    cacheWrite: z.number().nonnegative().optional(),
})
    .optional();
const ModelSchema = z
    .object({
    id: z.string().trim().min(1),
    name: z.string().trim().min(1).optional(),
    api: z.string().trim().min(1).optional(),
    baseUrl: z.string().trim().min(1).optional(),
    headers: z.record(z.string(), z.string()).optional(),
    reasoning: z.boolean().optional(),
    input: z.array(z.enum(["text", "image"])).optional(),
    cost: CostSchema,
    contextWindow: z.number().int().positive().optional(),
    maxTokens: z.number().int().positive().optional(),
    compat: z.unknown().optional(),
})
    .passthrough();
const ProviderSchema = z
    .object({
    api: z.string().trim().min(1).optional(),
    baseUrl: z.string().trim().min(1).optional(),
    headers: z.record(z.string(), z.string()).optional(),
    compat: z.unknown().optional(),
    models: z.array(ModelSchema).optional(),
})
    .passthrough();
const ModelsFileSchema = z
    .object({
    providers: z.record(z.string(), ProviderSchema),
})
    .passthrough();
let catalogState;
let extensionApiProvidersRegistered = false;
function registerExtensionApiProvidersOnce() {
    if (extensionApiProvidersRegistered) {
        return;
    }
    for (const extension of TAU_PROVIDER_EXTENSIONS) {
        extension.registerApiProviders?.();
    }
    extensionApiProvidersRegistered = true;
}
function ensureProviderModels(providers, provider) {
    const existing = providers.get(provider);
    if (existing) {
        return existing;
    }
    const created = new Map();
    providers.set(provider, created);
    return created;
}
function registerModel(args) {
    const providerModels = ensureProviderModels(args.providers, args.provider);
    const existing = providerModels.get(args.model.id);
    if (existing) {
        throw new Error(`duplicate model registration for '${args.provider}:${args.model.id}' from ${args.source}`);
    }
    providerModels.set(args.model.id, args.model);
}
function createCatalogState() {
    registerExtensionApiProvidersOnce();
    const providers = new Map();
    for (const provider of getProviders()) {
        for (const model of getModels(provider)) {
            registerModel({ providers, provider, model, source: "pi-ai" });
        }
    }
    const apiKeyResolvers = new Map();
    for (const extension of TAU_PROVIDER_EXTENSIONS) {
        if (extension.resolveApiKey) {
            if (apiKeyResolvers.has(extension.id)) {
                throw new Error(`duplicate api key resolver registration for provider '${extension.id}'`);
            }
            apiKeyResolvers.set(extension.id, extension.resolveApiKey);
        }
        for (const model of extension.models) {
            if (model.provider !== extension.id) {
                throw new Error(`invalid model registration for provider '${extension.id}': model '${model.id}' declares provider '${model.provider}'`);
            }
            registerModel({
                providers,
                provider: extension.id,
                model,
                source: `tau extension '${extension.id}'`,
            });
        }
    }
    return {
        providers,
        apiKeyResolvers,
    };
}
function getCatalogState() {
    if (!catalogState) {
        catalogState = createCatalogState();
    }
    return catalogState;
}
function modelKey(provider, modelId) {
    return `${provider}\u0000${modelId}`;
}
function cloneCompat(value) {
    if (value === undefined) {
        return undefined;
    }
    try {
        return structuredClone(value);
    }
    catch {
        return value;
    }
}
function cloneModel(model) {
    return {
        ...model,
        input: [...model.input],
        cost: { ...model.cost },
        ...(model.headers ? { headers: { ...model.headers } } : {}),
        ...(model.compat !== undefined
            ? { compat: cloneCompat(model.compat) }
            : {}),
    };
}
function isObject(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function mergeCompat(base, patch) {
    if (patch === undefined) {
        return base;
    }
    if (isObject(base) && isObject(patch)) {
        return {
            ...base,
            ...patch,
        };
    }
    return patch;
}
function applyModelPatch(model, patch) {
    let next = cloneModel(model);
    if (patch.api !== undefined) {
        next = { ...next, api: patch.api };
    }
    if (patch.baseUrl !== undefined) {
        next = { ...next, baseUrl: patch.baseUrl };
    }
    if (patch.headers !== undefined) {
        next = {
            ...next,
            headers: {
                ...(next.headers ?? {}),
                ...patch.headers,
            },
        };
    }
    if (patch.compat !== undefined) {
        next = {
            ...next,
            compat: mergeCompat(next.compat, patch.compat),
        };
    }
    if (patch.name !== undefined) {
        next = { ...next, name: patch.name };
    }
    if (patch.reasoning !== undefined) {
        next = { ...next, reasoning: patch.reasoning };
    }
    if (patch.input !== undefined) {
        next = { ...next, input: [...patch.input] };
    }
    if (patch.cost !== undefined) {
        next = {
            ...next,
            cost: {
                ...next.cost,
                ...patch.cost,
            },
        };
    }
    if (patch.contextWindow !== undefined) {
        next = { ...next, contextWindow: patch.contextWindow };
    }
    if (patch.maxTokens !== undefined) {
        next = { ...next, maxTokens: patch.maxTokens };
    }
    return next;
}
function mergeProviderPatch(base, overlay) {
    const mergedHeaders = {
        ...(base?.headers ?? {}),
        ...(overlay.headers ?? {}),
    };
    return {
        ...(base ?? {}),
        ...overlay,
        ...(Object.keys(mergedHeaders).length > 0 ? { headers: mergedHeaders } : {}),
        compat: mergeCompat(base?.compat, overlay.compat),
    };
}
function createProviderPatch(provider) {
    return {
        ...(provider.api !== undefined ? { api: provider.api } : {}),
        ...(provider.baseUrl !== undefined ? { baseUrl: provider.baseUrl } : {}),
        ...(provider.headers !== undefined ? { headers: provider.headers } : {}),
        ...(provider.compat !== undefined ? { compat: provider.compat } : {}),
    };
}
function createModelPatch(model) {
    return {
        ...(model.api !== undefined ? { api: model.api } : {}),
        ...(model.baseUrl !== undefined ? { baseUrl: model.baseUrl } : {}),
        ...(model.headers !== undefined ? { headers: model.headers } : {}),
        ...(model.compat !== undefined ? { compat: model.compat } : {}),
        ...(model.name !== undefined ? { name: model.name } : {}),
        ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
        ...(model.input !== undefined ? { input: model.input } : {}),
        ...(model.cost !== undefined ? { cost: model.cost } : {}),
        ...(model.contextWindow !== undefined ? { contextWindow: model.contextWindow } : {}),
        ...(model.maxTokens !== undefined ? { maxTokens: model.maxTokens } : {}),
    };
}
function createSyntheticModel(provider, modelId, providerTemplates) {
    const template = providerTemplates.get(provider);
    if (!template) {
        return undefined;
    }
    return {
        ...cloneModel(template),
        id: modelId,
        name: modelId,
    };
}
function formatZodError(error) {
    return error.issues
        .map((issue) => {
        const path = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
        return `${path}${issue.message}`;
    })
        .join("; ");
}
function loadModelsFile(path, deps) {
    let rawText;
    try {
        rawText = deps.fs.readFile(path);
    }
    catch (error) {
        return {
            error: `${path}: failed to read models file: ${error instanceof Error ? error.message : String(error)}`,
        };
    }
    let parsedJson;
    try {
        parsedJson = JSON.parse(rawText);
    }
    catch (error) {
        return {
            error: `${path}: failed to parse json: ${error instanceof Error ? error.message : String(error)}`,
        };
    }
    const parsed = ModelsFileSchema.safeParse(parsedJson);
    if (!parsed.success) {
        return {
            error: `${path}: invalid models schema: ${formatZodError(parsed.error)}`,
        };
    }
    return { data: parsed.data };
}
export function loadModelResolver(options) {
    const deps = options.deps;
    const levels = options.levels;
    const state = getCatalogState();
    const knownProviders = new Set(state.providers.keys());
    const modelsByKey = new Map();
    const providerTemplates = new Map();
    for (const [provider, providerModels] of state.providers.entries()) {
        const models = [...providerModels.values()];
        if (models.length === 0) {
            continue;
        }
        providerTemplates.set(provider, cloneModel(models[0]));
        for (const model of models) {
            modelsByKey.set(modelKey(provider, model.id), cloneModel(model));
        }
    }
    const providerPatches = new Map();
    const errors = [];
    for (const level of levels) {
        if (!deps.fs.exists(level.modelsPath)) {
            continue;
        }
        const loaded = loadModelsFile(level.modelsPath, deps);
        if (loaded.error) {
            errors.push(loaded.error);
            continue;
        }
        if (!loaded.data) {
            continue;
        }
        for (const [providerNameRaw, providerConfig] of Object.entries(loaded.data.providers)) {
            const providerName = providerNameRaw.trim().toLowerCase();
            if (!providerName || !knownProviders.has(providerName)) {
                errors.push(`${level.modelsPath}: unknown provider '${providerNameRaw}'.`);
                continue;
            }
            const providerPatch = createProviderPatch(providerConfig);
            if (Object.keys(providerPatch).length > 0) {
                const mergedProviderPatch = mergeProviderPatch(providerPatches.get(providerName), providerPatch);
                providerPatches.set(providerName, mergedProviderPatch);
                for (const [key, candidate] of modelsByKey.entries()) {
                    if (candidate.provider !== providerName) {
                        continue;
                    }
                    modelsByKey.set(key, applyModelPatch(candidate, mergedProviderPatch));
                }
            }
            for (const modelConfig of providerConfig.models ?? []) {
                const key = modelKey(providerName, modelConfig.id);
                const providerPatchForModel = providerPatches.get(providerName);
                const existing = modelsByKey.get(key) ??
                    createSyntheticModel(providerName, modelConfig.id, providerTemplates);
                if (!existing) {
                    errors.push(`${level.modelsPath}: provider '${providerName}' does not have bundled defaults to derive model '${modelConfig.id}'.`);
                    continue;
                }
                const withProviderDefaults = providerPatchForModel
                    ? applyModelPatch(existing, providerPatchForModel)
                    : existing;
                const next = applyModelPatch(withProviderDefaults, createModelPatch(modelConfig));
                modelsByKey.set(key, {
                    ...next,
                    id: modelConfig.id,
                    name: next.name || modelConfig.id,
                    provider: providerName,
                });
            }
        }
    }
    const resolveConfiguredModel = (providerRaw, modelIdRaw) => {
        const provider = providerRaw.trim().toLowerCase();
        const modelId = modelIdRaw.trim();
        if (!provider || !modelId || !knownProviders.has(provider)) {
            return undefined;
        }
        const existing = modelsByKey.get(modelKey(provider, modelId));
        if (!existing) {
            return undefined;
        }
        return cloneModel(existing);
    };
    const resolveModel = (providerRaw, modelIdRaw) => {
        const provider = providerRaw.trim().toLowerCase();
        const modelId = modelIdRaw.trim();
        if (!provider || !modelId || !knownProviders.has(provider)) {
            return undefined;
        }
        const existing = resolveConfiguredModel(provider, modelId);
        if (existing) {
            return existing;
        }
        const synthetic = createSyntheticModel(provider, modelId, providerTemplates);
        if (!synthetic) {
            return undefined;
        }
        const providerPatch = providerPatches.get(provider);
        if (!providerPatch) {
            return synthetic;
        }
        return applyModelPatch(synthetic, providerPatch);
    };
    return {
        resolveModel,
        resolveConfiguredModel,
        errors,
    };
}
export function listProviders() {
    return [...getCatalogState().providers.keys()];
}
export function listModels(provider) {
    const providerModels = getCatalogState().providers.get(provider);
    return providerModels ? [...providerModels.values()] : [];
}
export function resolveModel(provider, modelId) {
    return getCatalogState().providers.get(provider)?.get(modelId);
}
export function resolveModelOrThrow(provider, modelId) {
    const model = resolveModel(provider, modelId);
    if (!model) {
        throw new Error(`failed to resolve model '${provider}:${modelId}'`);
    }
    return model;
}
export function resolveProviderApiKey(args) {
    const resolver = getCatalogState().apiKeyResolvers.get(args.provider);
    if (!resolver) {
        return undefined;
    }
    return resolver(args);
}
//# sourceMappingURL=catalog.js.map