import { fetchWithDnsPinnedAddresses } from "@steipete/summarize-core/content";
import { isNativeOrBoundGlobalFetch, markFetchAsDnsPinned, resolveDnsPinnedFetch, supportsDnsPinnedFetch, } from "@steipete/summarize-core/content";
import { normalizeTokenUsage, tallyCosts } from "tokentally";
import { fetch as undiciFetch } from "undici";
import { buildRunMetricsReport } from "../costs.js";
import { loadCachedLiteLlmCatalog, loadLiteLlmCatalog, resolveLiteLlmMaxInputTokensForModelId, resolveLiteLlmMaxOutputTokensForModelId, resolveLiteLlmPricingForModelId, } from "../pricing/litellm.js";
function explicitCallCostUsd(call) {
    if (typeof call.costUsd === "number" && Number.isFinite(call.costUsd))
        return call.costUsd;
    return null;
}
export function createRunMetrics({ env, fetchImpl, maxOutputTokensArg, }) {
    const llmCalls = [];
    let firecrawlRequests = 0;
    let apifyRequests = 0;
    const transcriptionCost = {
        value: null,
        label: null,
    };
    const setTranscriptionCost = (costUsd, label) => {
        transcriptionCost.value = costUsd;
        transcriptionCost.label = label;
    };
    let liteLlmCatalogPromise = null;
    let cachedLiteLlmCatalogPromise = null;
    const getLiteLlmCatalog = async () => {
        if (!liteLlmCatalogPromise) {
            liteLlmCatalogPromise = loadLiteLlmCatalog({
                env,
                fetchImpl: globalThis.fetch.bind(globalThis),
            });
        }
        const result = await liteLlmCatalogPromise;
        return result.catalog;
    };
    const getCachedLiteLlmCatalog = async () => {
        if (!cachedLiteLlmCatalogPromise) {
            cachedLiteLlmCatalogPromise = loadCachedLiteLlmCatalog({ env });
        }
        return cachedLiteLlmCatalogPromise;
    };
    const capMaxOutputTokensForModel = async ({ modelId, requested, }) => {
        const catalog = await getCachedLiteLlmCatalog();
        if (!catalog)
            return requested;
        const limit = resolveLiteLlmMaxOutputTokensForModelId(catalog, modelId);
        if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) {
            return Math.min(requested, limit);
        }
        return requested;
    };
    const resolveMaxOutputTokensForCall = async (modelId) => {
        if (typeof maxOutputTokensArg !== "number")
            return null;
        return capMaxOutputTokensForModel({ modelId, requested: maxOutputTokensArg });
    };
    const resolveMaxInputTokensForCall = async (modelId) => {
        const catalog = await getCachedLiteLlmCatalog();
        if (!catalog)
            return null;
        const limit = resolveLiteLlmMaxInputTokensForModelId(catalog, modelId);
        if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) {
            return limit;
        }
        return null;
    };
    const estimateCostUsd = async () => {
        const extraCosts = [
            typeof transcriptionCost.value === "number" && Number.isFinite(transcriptionCost.value)
                ? transcriptionCost.value
                : null,
        ].filter((value) => typeof value === "number");
        const extraTotal = extraCosts.length > 0 ? extraCosts.reduce((sum, value) => sum + value, 0) : 0;
        const hasExtra = extraCosts.length > 0;
        const explicitCosts = llmCalls
            .map((call) => explicitCallCostUsd(call))
            .filter((value) => typeof value === "number");
        const explicitTotal = explicitCosts.length > 0 ? explicitCosts.reduce((sum, value) => sum + value, 0) : 0;
        const callsWithoutExplicitCost = llmCalls.filter((call) => explicitCallCostUsd(call) === null);
        const hasUnknownLlmCost = callsWithoutExplicitCost.some((call) => {
            const promptTokens = call.usage?.promptTokens ?? null;
            const completionTokens = call.usage?.completionTokens ?? null;
            return !(typeof promptTokens === "number" &&
                Number.isFinite(promptTokens) &&
                typeof completionTokens === "number" &&
                Number.isFinite(completionTokens));
        });
        if (hasUnknownLlmCost)
            return null;
        const calls = callsWithoutExplicitCost.map((call) => {
            const promptTokens = call.usage?.promptTokens ?? null;
            const completionTokens = call.usage?.completionTokens ?? null;
            const hasTokens = typeof promptTokens === "number" &&
                Number.isFinite(promptTokens) &&
                typeof completionTokens === "number" &&
                Number.isFinite(completionTokens);
            const usage = hasTokens
                ? normalizeTokenUsage({
                    inputTokens: promptTokens,
                    outputTokens: completionTokens,
                    totalTokens: call.usage?.totalTokens ?? undefined,
                })
                : null;
            return { model: call.model, usage };
        });
        if (calls.length === 0) {
            if (explicitCosts.length > 0 || hasExtra)
                return explicitTotal + extraTotal;
            return null;
        }
        const catalog = await getCachedLiteLlmCatalog();
        if (!catalog)
            return null;
        const result = await tallyCosts({
            calls,
            resolvePricing: (modelId) => resolveLiteLlmPricingForModelId(catalog, modelId),
        });
        if (Object.values(result.byModel).some((row) => row.cost === null))
            return null;
        const catalogTotal = result.total?.totalUsd ?? null;
        if (catalogTotal === null)
            return null;
        return catalogTotal + explicitTotal + extraTotal;
    };
    const buildReport = async () => {
        return buildRunMetricsReport({ llmCalls, firecrawlRequests, apifyRequests });
    };
    const recordFetch = (input) => {
        const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
        let hostname = null;
        try {
            hostname = new URL(url).hostname.toLowerCase();
        }
        catch {
            hostname = null;
        }
        if (hostname === "api.firecrawl.dev") {
            firecrawlRequests += 1;
        }
        else if (hostname === "api.apify.com") {
            apifyRequests += 1;
        }
    };
    const fetchAndTrack = async (targetFetch, input, init) => {
        recordFetch(input);
        return await targetFetch(input, init);
    };
    const trackedFetch = async (input, init) => {
        return await fetchAndTrack(fetchImpl, input, init);
    };
    const isBunRuntime = typeof process.versions.bun === "string";
    const isNativeFetch = isNativeOrBoundGlobalFetch(fetchImpl);
    const pinnedFetchImpl = resolveDnsPinnedFetch(fetchImpl) ??
        (isNativeFetch
            ? isBunRuntime
                ? fetchWithDnsPinnedAddresses
                : undiciFetch
            : null);
    if ((isNativeFetch || supportsDnsPinnedFetch(fetchImpl)) && pinnedFetchImpl) {
        const trackedPinnedFetch = async (input, init) => {
            return await fetchAndTrack(pinnedFetchImpl, input, init);
        };
        markFetchAsDnsPinned(trackedFetch, trackedPinnedFetch);
    }
    return {
        llmCalls,
        trackedFetch,
        buildReport,
        estimateCostUsd,
        getLiteLlmCatalog,
        resolveMaxOutputTokensForCall,
        resolveMaxInputTokensForCall,
        setTranscriptionCost,
    };
}
//# sourceMappingURL=metrics.js.map