import { z } from "zod";
import { isAbortError } from "../utils/errorHandling.js";
import { logger } from "../utils/logger.js";
/**
 * Model configuration schema for validation
 */
const ModelConfigSchema = z.object({
    id: z.string(),
    displayName: z.string(),
    capabilities: z.array(z.string()),
    deprecated: z.boolean(),
    pricing: z.object({
        input: z.number(),
        output: z.number(),
    }),
    contextWindow: z.number(),
    releaseDate: z.string(),
});
const ModelRegistrySchema = z.object({
    version: z.string(),
    lastUpdated: z.string(),
    models: z.record(z.string(), z.record(z.string(), ModelConfigSchema)),
    aliases: z.record(z.string(), z.string()).optional(),
    defaults: z.record(z.string(), z.string()).optional(),
});
/**
 * Dynamic Model Provider
 * Loads and manages model configurations from external sources
 */
export class DynamicModelProvider {
    static instance;
    modelRegistry = null;
    lastFetch = 0;
    CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
    constructor() { }
    static getInstance() {
        if (!this.instance) {
            this.instance = new DynamicModelProvider();
        }
        return this.instance;
    }
    /**
     * Initialize the model registry from multiple sources with timeout handling
     * Addresses hanging issues when localhost:3001 is not running or GitHub URLs timeout
     */
    async initialize() {
        const sources = [
            {
                url: process.env.MODEL_CONFIG_URL || "http://localhost:3001/api/v1/models",
                timeout: 3000, // 3s for localhost
                name: "local-server",
            },
            {
                url: `https://raw.githubusercontent.com/${process.env.MODEL_CONFIG_GITHUB_REPO || "juspay/neurolink"}/${process.env.MODEL_CONFIG_GITHUB_BRANCH || "release"}/config/models.json`,
                timeout: 5000, // 5s for GitHub
                name: "github-raw",
            },
            {
                url: "./config/models.json",
                timeout: 1000, // 1s for local file
                name: "local-file",
            },
        ];
        const errors = [];
        for (const source of sources) {
            try {
                logger.debug(`[DynamicModelProvider] Attempting to load from: ${source.url} (timeout: ${source.timeout}ms)`);
                const config = await this.loadFromSourceWithTimeout(source.url, source.timeout);
                // Validate the configuration
                const validatedConfig = ModelRegistrySchema.parse(config);
                this.modelRegistry = validatedConfig;
                this.lastFetch = Date.now();
                logger.info(`[DynamicModelProvider] Successfully loaded model registry from: ${source.name}`, {
                    source: source.url,
                    modelCount: this.getTotalModelCount(),
                    providerCount: Object.keys(validatedConfig.models).length,
                    loadTime: `<${source.timeout}ms`,
                });
                return; // Success, stop trying other sources
            }
            catch (error) {
                const errorMessage = error instanceof Error ? error.message : String(error);
                errors.push({ source: source.name, error: errorMessage });
                logger.warn(`[DynamicModelProvider] Failed to load from ${source.name} (${source.url}):`, {
                    error: errorMessage,
                    timeout: source.timeout,
                });
                continue;
            }
        }
        // Log all failures for debugging
        logger.warn(`[DynamicModelProvider] All model configuration sources failed`, { errors });
        throw new Error(`Failed to load model configuration from all source. Attempted: ${errors.map((e) => e.source).join(", ")}`);
    }
    /**
     * Load configuration from a source with timeout handling
     * Prevents hanging when local servers are down or network requests timeout
     */
    async loadFromSourceWithTimeout(source, timeoutMs) {
        if (source.startsWith("http")) {
            // Setup timeout and abort controller
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
            try {
                // Add health check for localhost before attempting full request
                if (source.includes("localhost") || source.includes("127.0.0.1")) {
                    await this.healthCheckLocalhost(source, Math.min(timeoutMs, 1000));
                }
                const response = await fetch(source, {
                    headers: {
                        "User-Agent": "NeuroLink/1.0 (+https://github.com/juspay/neurolink)",
                        Accept: "application/json",
                        "Cache-Control": "no-cache",
                    },
                    signal: controller.signal,
                });
                clearTimeout(timeoutId);
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                }
                const data = await response.json();
                return data;
            }
            catch (error) {
                clearTimeout(timeoutId);
                if (isAbortError(error)) {
                    throw new Error(`Request timeout after ${timeoutMs}ms`, {
                        cause: error,
                    });
                }
                throw error;
            }
        }
        else {
            // Load from local file with timeout (for very large files)
            return new Promise((resolve, reject) => {
                const timeoutId = setTimeout(() => {
                    reject(new Error(`File read timeout after ${timeoutMs}ms`));
                }, timeoutMs);
                (async () => {
                    try {
                        const fs = await import("fs");
                        const path = await import("path");
                        const fullPath = path.resolve(source);
                        // Check if file exists first
                        if (!fs.existsSync(fullPath)) {
                            throw new Error(`File not found: ${fullPath}`);
                        }
                        const content = fs.readFileSync(fullPath, "utf8");
                        const data = JSON.parse(content);
                        clearTimeout(timeoutId);
                        resolve(data);
                    }
                    catch (error) {
                        clearTimeout(timeoutId);
                        reject(error);
                    }
                })();
            });
        }
    }
    /**
     * Quick health check for localhost endpoints
     * Prevents hanging on non-responsive local servers
     */
    async healthCheckLocalhost(url, timeoutMs) {
        const healthUrl = url.replace(/\/api\/.*$/, "/health") || `${url}/health`;
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
        try {
            const response = await fetch(healthUrl, {
                method: "HEAD", // Lightweight request
                signal: controller.signal,
            });
            clearTimeout(timeoutId);
            // Don't throw on 404 - the main endpoint might still work
            if (response.status >= 500) {
                throw new Error(`Server error: ${response.status}`);
            }
        }
        catch (error) {
            clearTimeout(timeoutId);
            if (isAbortError(error)) {
                throw new Error(`Localhost health check timeout - server may not be running`, { cause: error });
            }
            // For connection refused, throw a more specific error
            if (error instanceof Error && error.message.includes("ECONNREFUSED")) {
                throw new Error(`Localhost server not running at ${url}`, {
                    cause: error,
                });
            }
            // For other errors, let the main request handle them
            logger.debug(`Health check failed for ${url}, proceeding with main request`, {
                error: error instanceof Error ? error.message : String(error),
            });
        }
    }
    /**
     * Get all available models for a provider
     */
    getModelsForProvider(provider) {
        this.ensureInitialized();
        return this.modelRegistry?.models[provider] || {};
    }
    /**
     * Resolve a model by provider and model hint
     */
    resolveModel(provider, modelHint) {
        this.ensureInitialized();
        const providerModels = this.getModelsForProvider(provider);
        if (!modelHint) {
            // Use default model for provider
            const defaultModel = this.modelRegistry?.defaults?.[provider];
            return defaultModel ? providerModels[defaultModel] : null;
        }
        // Check for exact match
        if (providerModels[modelHint]) {
            return providerModels[modelHint];
        }
        // Check aliases
        const aliasTarget = this.modelRegistry?.aliases?.[modelHint];
        if (aliasTarget) {
            const [aliasProvider, aliasModel] = aliasTarget.split("/");
            return this.resolveModel(aliasProvider, aliasModel);
        }
        // Fuzzy matching (partial string match)
        const fuzzyMatch = Object.keys(providerModels).find((key) => key.toLowerCase().includes(modelHint.toLowerCase()) ||
            modelHint.toLowerCase().includes(key.toLowerCase()));
        return fuzzyMatch ? providerModels[fuzzyMatch] : null;
    }
    /**
     * Search models by capabilities
     */
    searchByCapability(capability, options = {}) {
        this.ensureInitialized();
        const results = [];
        if (!this.modelRegistry) {
            return results;
        }
        for (const [providerName, models] of Object.entries(this.modelRegistry.models)) {
            if (options.provider && providerName !== options.provider) {
                continue;
            }
            for (const [modelName, modelConfig] of Object.entries(models)) {
                if (options.excludeDeprecated && modelConfig.deprecated) {
                    continue;
                }
                if (options.maxPrice && modelConfig.pricing.input > options.maxPrice) {
                    continue;
                }
                if (!modelConfig.capabilities.includes(capability)) {
                    continue;
                }
                results.push({
                    provider: providerName,
                    model: modelName,
                    config: modelConfig,
                });
            }
        }
        // Sort by price (cheapest first)
        return results.sort((a, b) => a.config.pricing.input - b.config.pricing.input);
    }
    /**
     * Get the best model for a specific use case
     */
    getBestModelFor(useCase) {
        this.ensureInitialized();
        switch (useCase) {
            case "coding":
                return (this.searchByCapability("functionCalling", {
                    excludeDeprecated: true,
                })[0] || null);
            case "analysis":
                return (this.searchByCapability("analysis", { excludeDeprecated: true })[0] ||
                    null);
            case "vision":
                return (this.searchByCapability("vision", { excludeDeprecated: true })[0] ||
                    null);
            case "fastest":
                // Return cheapest as proxy for fastest (usually correlates)
                return (this.getAllModels()
                    .filter((m) => !m.config.deprecated)
                    .sort((a, b) => a.config.pricing.input - b.config.pricing.input)[0] || null);
            case "cheapest":
                return (this.getAllModels()
                    .filter((m) => !m.config.deprecated)
                    .sort((a, b) => a.config.pricing.input - b.config.pricing.input)[0] || null);
            default:
                return null;
        }
    }
    /**
     * Get all models across all providers
     */
    getAllModels() {
        this.ensureInitialized();
        const results = [];
        if (!this.modelRegistry) {
            return results;
        }
        for (const [providerName, models] of Object.entries(this.modelRegistry.models)) {
            for (const [modelName, modelConfig] of Object.entries(models)) {
                results.push({
                    provider: providerName,
                    model: modelName,
                    config: modelConfig,
                });
            }
        }
        return results;
    }
    /**
     * Get total number of models
     */
    getTotalModelCount() {
        if (!this.modelRegistry) {
            return 0;
        }
        return Object.values(this.modelRegistry.models).reduce((total, providerModels) => total + Object.keys(providerModels).length, 0);
    }
    /**
     * Check if cache needs refresh
     */
    needsRefresh() {
        return Date.now() - this.lastFetch > this.CACHE_DURATION;
    }
    /**
     * Force refresh the model registry
     */
    async refresh() {
        this.modelRegistry = null;
        await this.initialize();
    }
    /**
     * Ensure the registry is initialized
     */
    ensureInitialized() {
        if (!this.modelRegistry) {
            throw new Error("Model registry not initialized. Call initialize() first.");
        }
    }
    /**
     * Get registry metadata
     */
    getMetadata() {
        if (!this.modelRegistry) {
            return null;
        }
        return {
            version: this.modelRegistry.version,
            lastUpdated: this.modelRegistry.lastUpdated,
            modelCount: this.getTotalModelCount(),
        };
    }
}
// Export singleton instance
export const dynamicModelProvider = DynamicModelProvider.getInstance();
//# sourceMappingURL=dynamicModels.js.map