// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// engine/llm-runtime/pricing-resolver.ts — SPEC-460: Resolve real pricing
// Strategy: fetch live from official URLs → cache 24h → local fallback only if offline.
// NEVER invent pricing. null when unknown.
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { detectModelFromEnv } from './client-detector.js';
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
/** Path for cached pricing (in project data, not in source) */
function getCachePath() {
    const configDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'config');
    return join(configDir, 'model-pricing.json');
}
/** Load local JSON (bundled fallback or cached fetch) */
function loadLocalPricing() {
    try {
        const raw = readFileSync(getCachePath(), 'utf-8');
        const data = JSON.parse(raw);
        if (data.providers && typeof data.providers === 'object') {
            return {
                lastFetchedAt: typeof data.lastFetchedAt === 'string'
                    ? data.lastFetchedAt
                    : typeof data.lastVerifiedAt === 'string'
                        ? data.lastVerifiedAt
                        : '',
                providers: data.providers,
            };
        }
        return null;
    }
    catch {
        return null;
    }
}
/** Check if cached data is still fresh */
function isCacheFresh(data) {
    if (!data.lastFetchedAt) {
        return false;
    }
    const age = Date.now() - new Date(data.lastFetchedAt).getTime();
    return age < CACHE_TTL_MS;
}
/** In-memory cache for the session */
let sessionPricing = null;
/**
 * Get pricing data — uses session cache, then local file cache, then live fetch.
 * Fire-and-forget background refresh when cache is stale.
 */
export function getPricingData() {
    // Session cache (fastest)
    if (sessionPricing) {
        return sessionPricing;
    }
    // Local file cache
    const local = loadLocalPricing();
    if (local) {
        sessionPricing = local;
        // If stale, trigger background refresh (non-blocking)
        if (!isCacheFresh(local)) {
            void refreshPricingFromWeb().catch(() => {
                /* best-effort */
            });
        }
        return sessionPricing;
    }
    // No cache at all — try sync fetch (blocking but only on first ever use)
    return null;
}
const PRICING_RAW_URL = 'https://raw.githubusercontent.com/planu-dev/planu/main/src/config/model-pricing.json';
/**
 * Fetch latest pricing from GitHub (latest main branch).
 * Runs in background when local cache is stale (>24h).
 * If fetch fails → silently keeps using bundled version.
 */
export async function refreshPricingFromWeb() {
    try {
        const controller = new AbortController();
        const timer = setTimeout(() => {
            controller.abort();
        }, 5000);
        const res = await fetch(PRICING_RAW_URL, { signal: controller.signal });
        clearTimeout(timer);
        if (!res.ok) {
            return;
        }
        const data = (await res.json());
        if (data.providers && typeof data.providers === 'object') {
            sessionPricing = {
                lastFetchedAt: new Date().toISOString(),
                providers: data.providers,
            };
            // Persist to local cache for next session
            try {
                const { writeFileSync, mkdirSync } = await import('node:fs');
                const cachePath = getCachePath();
                mkdirSync(dirname(cachePath), { recursive: true });
                writeFileSync(cachePath, JSON.stringify({ ...data, lastFetchedAt: sessionPricing.lastFetchedAt }, null, 2), 'utf-8');
            }
            catch {
                // Cache write failure is not critical
            }
        }
    }
    catch {
        // Network failure — keep using bundled version
    }
}
// === Model detection ===
function detectProvider(modelId) {
    const lower = modelId.toLowerCase();
    if (lower.includes('claude')) {
        return 'anthropic';
    }
    if (lower.includes('gpt') ||
        lower.includes('o1') ||
        lower.includes('o3') ||
        lower.includes('o4') ||
        lower.includes('codex')) {
        return 'openai';
    }
    if (lower.includes('gemini')) {
        return 'google';
    }
    return null;
}
function findModelPricing(modelId, pricing) {
    const provider = detectProvider(modelId);
    if (!provider) {
        return null;
    }
    const providerModels = pricing.providers[provider];
    if (!providerModels) {
        return null;
    }
    // Exact match
    const exact = providerModels[modelId];
    if (exact) {
        return exact;
    }
    // Prefix match (e.g. "claude-opus-4-6-20260101" matches "claude-opus-4-6")
    const lower = modelId.toLowerCase();
    for (const [key, value] of Object.entries(providerModels)) {
        if (lower.startsWith(key)) {
            return value;
        }
    }
    return null;
}
// === Public API ===
/**
 * Resolve pricing for the current session.
 * Returns null if model unknown or pricing unavailable — NEVER invents.
 */
export function resolvePricing(explicitModelId) {
    const pricing = getPricingData();
    if (!pricing) {
        return null;
    }
    // Layer 1: explicit model ID
    if (explicitModelId) {
        const found = findModelPricing(explicitModelId, pricing);
        if (found) {
            return {
                modelId: explicitModelId,
                provider: detectProvider(explicitModelId) ?? 'unknown',
                ...found,
                detectionMethod: 'tool_param',
                pricingSource: isCacheFresh(pricing) ? 'cached' : 'bundled',
            };
        }
    }
    // Layer 2: env vars
    const envModel = detectModelFromEnv();
    if (envModel) {
        const found = findModelPricing(envModel, pricing);
        if (found) {
            return {
                modelId: envModel,
                provider: detectProvider(envModel) ?? 'unknown',
                ...found,
                detectionMethod: 'env_var',
                pricingSource: isCacheFresh(pricing) ? 'cached' : 'bundled',
            };
        }
    }
    // Layer 3: client hint → only tells provider, not model. Can't price.
    return null;
}
/**
 * Calculate real cost from measured tokens. Returns null if pricing unavailable.
 */
export function calculateRealCost(inputTokens, outputTokens, pricing) {
    const inputCost = (inputTokens / 1_000_000) * pricing.inputPerMToken;
    const outputCost = (outputTokens / 1_000_000) * pricing.outputPerMToken;
    return Math.round((inputCost + outputCost) * 10000) / 10000;
}
export function getPricingAge() {
    return getPricingData()?.lastFetchedAt ?? null;
}
export function resetPricingCache() {
    sessionPricing = null;
}
//# sourceMappingURL=pricing-resolver.js.map