import { dirname, join } from 'node:path';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { logger } from '../utils/logger.js';
import { summarizeError } from '../utils/error-display.js';
import { instrumentedFetch } from '../utils/fetch-with-timeout.js';
const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models';
const FETCH_TIMEOUT_MS = 15_000;
const CACHE_TTL_MS = 86_400_000;
const DEFAULT_TOKEN_LIMITS = {
    maxOutputTokens: 8192,
    maxToolResultTokens: 50_000,
    maxToolCalls: 128,
    maxReasoningTokens: 16384,
};
export function getModelLimitsCachePath(cacheDir) {
    return join(cacheDir, 'model-limits.json');
}
function validateModelLimitsCache(value) {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
        return { cache: null, reason: 'root value is not an object' };
    }
    const parsed = value;
    if (parsed.version !== 1)
        return { cache: null, reason: 'unsupported cache version' };
    if (typeof parsed.fetchedAt !== 'number' || !Number.isFinite(parsed.fetchedAt)) {
        return { cache: null, reason: 'fetchedAt must be a finite number' };
    }
    if (typeof parsed.ttlMs !== 'number' || !Number.isFinite(parsed.ttlMs)) {
        return { cache: null, reason: 'ttlMs must be a finite number' };
    }
    if (!parsed.models || typeof parsed.models !== 'object' || Array.isArray(parsed.models)) {
        return { cache: null, reason: 'models must be an object' };
    }
    return { cache: parsed };
}
function loadCachedLimits(cachePath) {
    try {
        const raw = readFileSync(cachePath, 'utf-8');
        const parsed = JSON.parse(raw);
        const { cache, reason } = validateModelLimitsCache(parsed);
        if (!cache) {
            logger.warn('[model-limits] Ignoring malformed cache', { cachePath, reason: reason ?? 'unknown' });
            return null;
        }
        return cache;
    }
    catch (error) {
        const message = summarizeError(error);
        if (message.includes('ENOENT') || message.includes('no such file')) {
            logger.debug('[model-limits] No cache file found (first run)');
        }
        else {
            logger.warn('[model-limits] Cache load failed (corrupted?)', { error: message });
        }
        return null;
    }
}
function saveCachedLimits(cache, cachePath) {
    try {
        mkdirSync(dirname(cachePath), { recursive: true });
        writeFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
    }
    catch (error) {
        logger.warn('[model-limits] Cache write failed', { error: summarizeError(error) });
    }
}
function isCacheStale(cache) {
    return Date.now() - cache.fetchedAt > cache.ttlMs;
}
async function fetchOpenRouterModels() {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
    timer.unref?.();
    try {
        const response = await instrumentedFetch(OPENROUTER_MODELS_URL, {
            signal: controller.signal,
            headers: { Accept: 'application/json' },
        });
        if (!response.ok) {
            throw new Error(`OpenRouter API returned ${response.status}`);
        }
        const json = await response.json();
        const map = new Map();
        if (Array.isArray(json.data)) {
            for (const model of json.data) {
                if (typeof model.id === 'string') {
                    map.set(model.id, model);
                }
            }
        }
        return map;
    }
    finally {
        clearTimeout(timer);
    }
}
function getModelStem(modelId) {
    return modelId
        .replace(/-\d{8}$/, '')
        .replace(/-(?:2[4-9]|3[0-9])\d{2}$/, '')
        .replace(/-\d{6}$/, '');
}
function findOpenRouterMatch(modelId, provider, orModels) {
    if (orModels.has(modelId))
        return orModels.get(modelId) ?? null;
    const prefixed = `${provider}/${modelId}`;
    if (orModels.has(prefixed))
        return orModels.get(prefixed) ?? null;
    const stem = getModelStem(modelId);
    if (stem !== modelId) {
        if (orModels.has(stem))
            return orModels.get(stem) ?? null;
        const prefixedStem = `${provider}/${stem}`;
        if (orModels.has(prefixedStem))
            return orModels.get(prefixedStem) ?? null;
    }
    for (const [orId, orModel] of orModels) {
        if (orId.endsWith(`/${stem}`) || orId.endsWith(`/${modelId}`)) {
            return orModel;
        }
    }
    return null;
}
function buildOrMap(cache) {
    const map = new Map();
    for (const [id, entry] of Object.entries(cache.models)) {
        map.set(id, {
            id,
            context_length: entry.contextLength,
            top_provider: { max_completion_tokens: entry.maxOutputTokens },
            supported_parameters: entry.supportedParameters,
            pricing: entry.pricing
                ? { prompt: String(entry.pricing.prompt), completion: String(entry.pricing.completion) }
                : undefined,
        });
    }
    return map;
}
export class ModelLimitsService {
    options;
    cachedData = null;
    cachedOrMap = null;
    constructor(options) {
        this.options = options;
    }
    ensureOpenRouterMap() {
        if (!this.cachedData)
            return null;
        if (!this.cachedOrMap) {
            this.cachedOrMap = buildOrMap(this.cachedData);
        }
        return this.cachedOrMap;
    }
    resolveTokenLimits(modelDef, providerLimits) {
        const result = { ...DEFAULT_TOKEN_LIMITS };
        const orMap = this.ensureOpenRouterMap();
        if (orMap) {
            const orMatch = findOpenRouterMatch(modelDef.id, modelDef.provider, orMap);
            if (orMatch?.top_provider?.max_completion_tokens != null) {
                result.maxOutputTokens = orMatch.top_provider.max_completion_tokens;
            }
        }
        if (providerLimits) {
            if (providerLimits.maxOutputTokens != null)
                result.maxOutputTokens = providerLimits.maxOutputTokens;
            if (providerLimits.maxToolResultTokens != null)
                result.maxToolResultTokens = providerLimits.maxToolResultTokens;
            if (providerLimits.maxToolCalls != null)
                result.maxToolCalls = providerLimits.maxToolCalls;
            if (providerLimits.maxReasoningTokens != null)
                result.maxReasoningTokens = providerLimits.maxReasoningTokens;
        }
        const explicit = modelDef.tokenLimits;
        if (explicit) {
            if (explicit.maxOutputTokens != null)
                result.maxOutputTokens = explicit.maxOutputTokens;
            if (explicit.maxToolResultTokens != null)
                result.maxToolResultTokens = explicit.maxToolResultTokens;
            if (explicit.maxToolCalls != null)
                result.maxToolCalls = explicit.maxToolCalls;
            if (explicit.maxReasoningTokens != null)
                result.maxReasoningTokens = explicit.maxReasoningTokens;
        }
        return result;
    }
    getPricingForModel(modelId, provider) {
        const orMap = this.ensureOpenRouterMap();
        if (!orMap)
            return null;
        const match = findOpenRouterMatch(modelId, provider, orMap);
        if (!match?.pricing)
            return null;
        const prompt = parseFloat(match.pricing.prompt);
        const completion = parseFloat(match.pricing.completion);
        if (Number.isNaN(prompt) || Number.isNaN(completion))
            return null;
        return { prompt, completion };
    }
    getTokenLimitsForModel(modelDef) {
        return this.resolveTokenLimits(modelDef);
    }
    getContextWindowForModel(modelDef) {
        if (modelDef.contextWindowProvenance === 'provider_api' && modelDef.contextWindow > 0) {
            return modelDef.contextWindow;
        }
        const orMap = this.ensureOpenRouterMap();
        if (orMap) {
            const orMatch = findOpenRouterMatch(modelDef.id, modelDef.provider, orMap);
            if (orMatch?.context_length != null && orMatch.context_length > 0) {
                return orMatch.context_length;
            }
        }
        return modelDef.contextWindow;
    }
    getToolResultMaxCharsForModel(model) {
        if (!model)
            return DEFAULT_TOKEN_LIMITS.maxToolResultTokens;
        return this.resolveTokenLimits(model).maxToolResultTokens;
    }
    async refresh() {
        const orModels = await fetchOpenRouterModels();
        const models = {};
        for (const [id, model] of orModels) {
            let pricing;
            if (model.pricing?.prompt != null && model.pricing?.completion != null) {
                const prompt = parseFloat(model.pricing.prompt);
                const completion = parseFloat(model.pricing.completion);
                if (!Number.isNaN(prompt) && !Number.isNaN(completion)) {
                    pricing = { prompt, completion };
                }
            }
            models[id] = {
                contextLength: model.context_length ?? 0,
                maxOutputTokens: model.top_provider?.max_completion_tokens ?? null,
                supportedParameters: model.supported_parameters ?? [],
                pricing,
            };
        }
        const newCache = {
            version: 1,
            fetchedAt: Date.now(),
            ttlMs: CACHE_TTL_MS,
            models,
        };
        saveCachedLimits(newCache, this.options.cachePath);
        this.cachedData = newCache;
        this.cachedOrMap = buildOrMap(newCache);
        return orModels.size;
    }
    init() {
        this.cachedData = loadCachedLimits(this.options.cachePath);
        this.cachedOrMap = this.cachedData ? buildOrMap(this.cachedData) : null;
        if (!this.cachedData || isCacheStale(this.cachedData)) {
            void this.refresh().catch((error) => {
                logger.warn('[model-limits] Background refresh failed', { error: summarizeError(error) });
            });
        }
    }
}
