import { lazyStream, } from "@earendil-works/pi-ai";
import { getApiProvider } from "@earendil-works/pi-ai/compat";
import { clearConfigValueCache, getConfigValueEnvVarNames, isCommandConfigValue, isConfigValueConfigured, resolveConfigValueOrThrow, resolveHeadersOrThrow, } from "./resolve-config-value.js";
export const clearApiKeyCache = clearConfigValueCache;
function mergeCompat(base, override) {
    if (!override)
        return base;
    const merged = { ...base, ...override };
    const baseNested = base;
    const overrideNested = override;
    const mergedNested = merged;
    for (const key of ["openRouterRouting", "vercelGatewayRouting", "chatTemplateKwargs", "chatTemplateArgs"]) {
        const baseValue = baseNested?.[key];
        const overrideValue = overrideNested[key];
        if ((typeof baseValue === "object" && baseValue !== null) ||
            (typeof overrideValue === "object" && overrideValue !== null)) {
            mergedNested[key] = { ...baseValue, ...overrideValue };
        }
    }
    return merged;
}
function applyModelOverride(model, override) {
    return {
        ...model,
        name: override.name ?? model.name,
        reasoning: override.reasoning ?? model.reasoning,
        thinkingLevelMap: override.thinkingLevelMap
            ? { ...model.thinkingLevelMap, ...override.thinkingLevelMap }
            : model.thinkingLevelMap,
        input: override.input ?? model.input,
        cost: override.cost
            ? {
                input: override.cost.input ?? model.cost.input,
                output: override.cost.output ?? model.cost.output,
                cacheRead: override.cost.cacheRead ?? model.cost.cacheRead,
                cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,
                tiers: override.cost.tiers ?? model.cost.tiers,
            }
            : model.cost,
        contextWindow: override.contextWindow ?? model.contextWindow,
        maxTokens: override.maxTokens ?? model.maxTokens,
        samplingParams: override.samplingParams
            ? { ...model.samplingParams, ...override.samplingParams }
            : model.samplingParams,
        compat: mergeCompat(model.compat, override.compat),
    };
}
function modelFromJson(providerId, definition, providerConfig, defaults) {
    const api = definition.api ?? providerConfig.api ?? defaults?.api;
    if (!api) {
        throw new Error(`Provider ${providerId}, model ${definition.id}: no "api" specified. Set at provider or model level.`);
    }
    const baseUrl = definition.baseUrl ?? providerConfig.baseUrl ?? defaults?.baseUrl;
    if (!baseUrl)
        throw new Error(`Provider ${providerId}: "baseUrl" is required when defining custom models.`);
    if (definition.contextWindow !== undefined && definition.contextWindow <= 0) {
        throw new Error(`Provider ${providerId}, model ${definition.id}: invalid contextWindow`);
    }
    if (definition.maxTokens !== undefined && definition.maxTokens <= 0) {
        throw new Error(`Provider ${providerId}, model ${definition.id}: invalid maxTokens`);
    }
    return {
        id: definition.id,
        name: definition.name ?? definition.id,
        api: api,
        provider: providerId,
        baseUrl,
        reasoning: definition.reasoning ?? false,
        thinkingLevelMap: definition.thinkingLevelMap,
        input: (definition.input ?? ["text"]),
        cost: definition.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
        contextWindow: definition.contextWindow ?? 128000,
        maxTokens: definition.maxTokens ?? 16384,
        samplingParams: definition.samplingParams,
        headers: undefined,
        compat: mergeCompat(providerConfig.compat, definition.compat),
    };
}
function applyModelsJson(providerId, baseModels, config) {
    if (!config)
        return [...baseModels];
    if (config.oauth && !config.baseUrl) {
        throw new Error(`Provider ${providerId}: "baseUrl" is required when "oauth" is set.`);
    }
    const hasOverrides = config.modelOverrides && Object.keys(config.modelOverrides).length > 0;
    if (!config.models?.length &&
        !config.baseUrl &&
        !config.headers &&
        !config.compat &&
        !hasOverrides &&
        !config.apiKey &&
        !config.oauth &&
        config.authHeader === undefined) {
        throw new Error(`Provider ${providerId}: must specify "baseUrl", "headers", "compat", "modelOverrides", or "models".`);
    }
    const models = baseModels.map((model) => ({
        ...model,
        baseUrl: config.oauth === "radius" ? model.baseUrl : (config.baseUrl ?? model.baseUrl),
        compat: mergeCompat(model.compat, config.compat),
    }));
    for (const definition of config.models ?? []) {
        const existingIndex = models.findIndex((model) => model.id === definition.id);
        const defaults = existingIndex >= 0 ? models[existingIndex] : models[0];
        const model = modelFromJson(providerId, definition, config, defaults);
        if (existingIndex >= 0)
            models[existingIndex] = model;
        else
            models.push(model);
    }
    return models;
}
function applyExtension(providerId, models, config) {
    if (!config)
        return [...models];
    if (!config.models) {
        return config.baseUrl ? models.map((model) => ({ ...model, baseUrl: config.baseUrl })) : [...models];
    }
    return config.models.map((definition) => {
        const defaults = models.find((model) => model.id === definition.id) ?? models[0];
        const api = definition.api ?? config.api ?? defaults?.api;
        if (!api) {
            throw new Error(`Provider ${providerId}, model ${definition.id}: no "api" specified. Set at provider or model level.`);
        }
        const baseUrl = definition.baseUrl ?? config.baseUrl ?? defaults?.baseUrl;
        if (!baseUrl)
            throw new Error(`Provider ${providerId}: "baseUrl" is required when defining custom models.`);
        return {
            ...definition,
            api,
            provider: providerId,
            baseUrl,
            headers: undefined,
        };
    });
}
function adaptOAuth(config) {
    return {
        name: config.name,
        isSubscription: config.isSubscription,
        login: async (callbacks) => {
            const credential = await config.login({
                onAuth: (info) => callbacks.notify({ type: "auth_url", ...info }),
                onDeviceCode: (info) => callbacks.notify({ type: "device_code", ...info }),
                onPrompt: (prompt) => callbacks.prompt({ type: "text", ...prompt }),
                onProgress: (message) => callbacks.notify({ type: "progress", message }),
                onManualCodeInput: () => callbacks.prompt({ type: "manual_code", message: "Paste the authorization code" }),
                onSelect: (prompt) => callbacks.prompt({ type: "select", ...prompt }),
                signal: callbacks.signal,
            });
            return { ...credential, type: "oauth" };
        },
        refresh: async (credential, signal) => ({ ...(await config.refreshToken(credential, signal)), type: "oauth" }),
        toAuth: async (credential) => ({ apiKey: config.getApiKey(credential) }),
    };
}
function withConfiguredAuth(auth, headers, authHeader) {
    let mergedHeaders = auth.headers || headers ? { ...auth.headers, ...headers } : undefined;
    if (authHeader) {
        if (!auth.apiKey)
            throw new Error("authHeader requires a resolved API key");
        mergedHeaders = { ...mergedHeaders, Authorization: `Bearer ${auth.apiKey}` };
    }
    return { ...auth, headers: mergedHeaders };
}
function configuredApiKey(config, extension) {
    return extension?.apiKey ?? config?.apiKey;
}
function configuredHeaders(config, extension) {
    if (!config?.headers && !extension?.headers)
        return undefined;
    return { ...config?.headers, ...extension?.headers };
}
async function configContextEnv(values, ctx, explicit) {
    const env = { ...explicit };
    for (const name of new Set(values.flatMap(getConfigValueEnvVarNames))) {
        if (env[name] !== undefined)
            continue;
        const value = await ctx.env(name);
        if (value !== undefined)
            env[name] = value;
    }
    return Object.keys(env).length > 0 ? env : undefined;
}
function composeApiKeyAuth(providerId, base, config, extension) {
    const inherited = base?.auth.apiKey;
    const rawKey = configuredApiKey(config, extension);
    const oauth = extension?.oauth ?? base?.auth.oauth;
    // OAuth-only providers get no fabricated API-key login method.
    if (!inherited && rawKey === undefined && oauth)
        return undefined;
    const rawHeaders = configuredHeaders(config, extension);
    const authHeader = extension?.authHeader ?? config?.authHeader ?? false;
    return {
        name: inherited?.name ?? "API key",
        login: inherited?.login ??
            (async (interaction) => ({
                type: "api_key",
                key: await interaction.prompt({ type: "secret", message: "Enter API key" }),
            })),
        check: async (input) => {
            if (input.credential) {
                if (inherited?.check)
                    return inherited.check(input);
                if (input.credential.key)
                    return { type: "api_key", source: "stored credential" };
                const resolved = await inherited?.resolve(input);
                return resolved ? { type: "api_key", source: resolved.source } : undefined;
            }
            if (rawKey !== undefined) {
                if (isCommandConfigValue(rawKey))
                    return { type: "api_key", source: "configured API key" };
                const envNames = getConfigValueEnvVarNames(rawKey);
                for (const name of envNames) {
                    if ((await input.ctx.env(name)) === undefined)
                        return undefined;
                }
                return { type: "api_key", source: "configured API key" };
            }
            if (inherited?.check)
                return inherited.check(input);
            const resolved = await inherited?.resolve(input);
            return resolved ? { type: "api_key", source: resolved.source } : undefined;
        },
        resolve: async (input) => {
            let result;
            if (input.credential) {
                result = inherited
                    ? await inherited.resolve(input)
                    : input.credential.key
                        ? { auth: { apiKey: input.credential.key }, env: input.credential.env, source: "stored credential" }
                        : undefined;
            }
            else if (rawKey !== undefined) {
                const env = await configContextEnv([rawKey], input.ctx);
                const key = resolveConfigValueOrThrow(rawKey, `API key for provider "${providerId}"`, env);
                result = inherited
                    ? await inherited.resolve({ ...input, credential: { type: "api_key", key } })
                    : { auth: { apiKey: key }, source: "configured API key" };
            }
            else {
                result = await inherited?.resolve(input);
            }
            if (!result)
                return undefined;
            const explicitEnv = { ...(input.credential?.env ?? {}), ...(result.env ?? {}) };
            const headerEnv = await configContextEnv(Object.values(rawHeaders ?? {}), input.ctx, explicitEnv);
            const headers = resolveHeadersOrThrow(rawHeaders, `provider "${providerId}"`, headerEnv);
            return { ...result, auth: withConfiguredAuth(result.auth, headers, authHeader) };
        },
    };
}
function composeOAuthAuth(providerId, base, config, extension) {
    const oauth = extension?.oauth ? adaptOAuth(extension.oauth) : base?.auth.oauth;
    if (!oauth)
        return undefined;
    const rawHeaders = configuredHeaders(config, extension);
    const authHeader = extension?.authHeader ?? config?.authHeader ?? false;
    return {
        ...oauth,
        toAuth: async (credential) => {
            const auth = await oauth.toAuth(credential);
            const env = credential.env;
            const headers = resolveHeadersOrThrow(rawHeaders, `provider "${providerId}"`, typeof env === "object" && env !== null ? env : undefined);
            return withConfiguredAuth(auth, headers, authHeader);
        },
    };
}
function rawModelHeaders(model, config, extension) {
    const definition = config?.models?.find((entry) => entry.id === model.id);
    const extensionModel = extension?.models?.find((entry) => entry.id === model.id);
    const headers = {
        ...config?.modelOverrides?.[model.id]?.headers,
        ...definition?.headers,
        ...extensionModel?.headers,
    };
    return Object.keys(headers).length > 0 ? headers : undefined;
}
export function validateExtensionProvider(providerId, base, modelsConfig, extension) {
    if (extension.streamSimple && !extension.api) {
        throw new Error(`Provider ${providerId}: "api" is required when registering streamSimple.`);
    }
    applyExtension(providerId, applyModelsJson(providerId, base?.getModels() ?? [], modelsConfig), extension);
}
/** Compose built-in, models.json, and extension layers without reading credentials. */
export function composeModelProvider(providerId, base, modelConfig, extension) {
    const config = modelConfig.getProvider(providerId);
    let extensionOAuthCredential;
    let refreshedExtensionModels;
    const currentExtension = () => extension && refreshedExtensionModels ? { ...extension, models: refreshedExtensionModels } : extension;
    // models.json modelOverrides are the topmost user-config layer: they apply once,
    // after custom-model upserts, extension model replacement, and legacy OAuth projection.
    const getModels = () => {
        let models = applyExtension(providerId, applyModelsJson(providerId, base?.getModels() ?? [], config), currentExtension());
        if (extensionOAuthCredential && extension?.oauth?.modifyModels) {
            models = extension.oauth.modifyModels(models, extensionOAuthCredential);
        }
        return models.map((model) => {
            const override = config?.modelOverrides?.[model.id];
            return override ? applyModelOverride(model, override) : model;
        });
    };
    // Validate eagerly so registration/reload reports structural errors immediately.
    getModels();
    const apiKey = composeApiKeyAuth(providerId, base, config, extension);
    const oauth = composeOAuthAuth(providerId, base, config, extension);
    if (!apiKey && !oauth)
        throw new Error(`Provider ${providerId}: no authentication method configured.`);
    const supportsBaseApi = (model) => base?.getModels().some((entry) => entry.api === model.api) ?? false;
    const streamWith = (model, context, options, simple) => lazyStream(model, async () => {
        if (extension?.streamSimple && model.api === extension.api) {
            return extension.streamSimple(model, context, options);
        }
        if (base && supportsBaseApi(model)) {
            return simple
                ? base.streamSimple(model, context, options)
                : base.stream(model, context, options);
        }
        const api = getApiProvider(model.api);
        if (!api)
            throw new Error(`No API provider registered for api: ${model.api}`);
        return simple
            ? api.streamSimple(model, context, options)
            : api.stream(model, context, options);
    });
    const provider = {
        id: providerId,
        name: extension?.name ?? config?.name ?? base?.name ?? extension?.oauth?.name ?? providerId,
        baseUrl: extension?.baseUrl ?? config?.baseUrl ?? base?.baseUrl,
        headers: base?.headers,
        auth: { ...(apiKey ? { apiKey } : {}), ...(oauth ? { oauth } : {}) },
        getModels,
        refreshModels: base?.refreshModels || extension?.refreshModels || extension?.oauth?.modifyModels
            ? async (context) => {
                await base?.refreshModels?.(context);
                let refreshed;
                if (extension?.refreshModels)
                    refreshed = await extension.refreshModels(context);
                if (context.signal.aborted)
                    return;
                const oauthCredential = context.credential?.type === "oauth" ? context.credential : undefined;
                await context.publish({
                    update: () => {
                        if (refreshed) {
                            // Validate before publishing the new synchronous list.
                            applyExtension(providerId, applyModelsJson(providerId, base?.getModels() ?? [], config), {
                                ...extension,
                                models: refreshed,
                            });
                            refreshedExtensionModels = refreshed;
                        }
                        extensionOAuthCredential = oauthCredential;
                    },
                });
            }
            : undefined,
        filterModels: base?.filterModels
            ? (models, credential) => base.filterModels(models, credential)
            : undefined,
        stream: (model, context, options) => streamWith(model, context, options, false),
        streamSimple: (model, context, options) => streamWith(model, context, options, true),
    };
    const fetchDeferred = base?.fetchDeferred;
    if (fetchDeferred) {
        provider.fetchDeferred = (model, handle, options) => fetchDeferred(model, handle, options);
    }
    const cancelDeferred = base?.cancelDeferred;
    if (cancelDeferred) {
        provider.cancelDeferred = (model, handle, options) => cancelDeferred(model, handle, options);
    }
    return provider;
}
export function resolveConfiguredModelHeaders(model, config, extension, env) {
    return resolveHeadersOrThrow(rawModelHeaders(model, config, extension), `model "${model.provider}/${model.id}"`, env);
}
export function resolveCompatibilityRequestConfig(model, config, extension) {
    const configured = resolveHeadersOrThrow({ ...configuredHeaders(config, extension), ...rawModelHeaders(model, config, extension) }, `model "${model.provider}/${model.id}"`);
    return {
        headers: model.headers || configured ? { ...model.headers, ...configured } : undefined,
        authHeader: extension?.authHeader ?? config?.authHeader ?? false,
    };
}
export function configuredRequestAuthStatus(config, extension) {
    const value = configuredApiKey(config, extension);
    if (value === undefined)
        return undefined;
    if (isCommandConfigValue(value))
        return { configured: true, source: "models_json_command" };
    const names = getConfigValueEnvVarNames(value);
    if (names.length > 0) {
        return isConfigValueConfigured(value)
            ? { configured: true, source: "environment", label: names.join(", ") }
            : { configured: false };
    }
    return { configured: true, source: extension?.apiKey !== undefined ? "fallback" : "models_json_key" };
}
//# sourceMappingURL=provider-composer.js.map