/**
 * Local LLM Providers
 *
 * Native JavaScript/TypeScript LLM providers for offline use:
 * - node-llama-cpp: Native Node.js bindings for llama.cpp
 * - WebLLM: Browser-based LLM with WebGPU
 * - Ollama: HTTP API to local Ollama server
 *
 * These providers allow Grok CLI to function without cloud API dependencies.
 */
import { EventEmitter } from 'events';
import fs from 'fs-extra';
import * as path from 'path';
import * as os from 'os';
import { logger } from '../utils/logger.js';
import { retry, RetryStrategies, RetryPredicates } from '../utils/retry.js';
import { safeStreamRead, handleStreamError } from '../utils/stream-helpers.js';
import { UserFriendlyError, ModelNotFoundError, ProviderNotAvailableError } from '../utils/errors.js';
// ============================================================================
// node-llama-cpp Provider
// ============================================================================
/**
 * Native Node.js LLM provider using node-llama-cpp
 *
 * Advantages:
 * - No external dependencies (Ollama not required)
 * - Direct C++ bindings = lowest latency
 * - Fine-grained control over model parameters
 * - Supports CUDA, Metal, and CPU inference
 *
 * Usage:
 * ```typescript
 * const provider = new NodeLlamaCppProvider();
 * await provider.initialize({ modelPath: './models/llama-3.1-8b.gguf' });
 * const response = await provider.complete([{ role: 'user', content: 'Hello!' }]);
 * ```
 */
export class NodeLlamaCppProvider extends EventEmitter {
    type = 'local-llama';
    name = 'node-llama-cpp';
    config = null;
    model = null;
    context = null;
    ready = false;
    modelsDir;
    constructor() {
        super();
        this.modelsDir = path.join(os.homedir(), '.codebuddy', 'models');
    }
    async initialize(config) {
        this.config = config;
        // Ensure models directory exists
        await fs.ensureDir(this.modelsDir);
        // Check if model path exists
        const modelPath = config.modelPath || path.join(this.modelsDir, 'llama-3.1-8b-q4_k_m.gguf');
        if (!await fs.pathExists(modelPath)) {
            // Get available models for helpful error message
            const availableModels = await this.getModels();
            throw new ModelNotFoundError({
                model: path.basename(modelPath),
                provider: 'local-llama',
                availableModels: availableModels.length > 0 ? availableModels : undefined,
                downloadCommand: `wget -P ~/.codebuddy/models/ https://huggingface.co/lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/resolve/main/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf`,
            });
        }
        try {
            // Dynamic import of node-llama-cpp (optional dependency)
            const nodeLlamaCpp = await import('node-llama-cpp').catch(() => null);
            if (!nodeLlamaCpp) {
                throw new ProviderNotAvailableError({
                    provider: 'node-llama-cpp',
                    reason: 'Package not installed',
                    installCommand: 'npm install node-llama-cpp',
                });
            }
            const { LlamaModel, LlamaContext } = nodeLlamaCpp;
            this.model = new LlamaModel({
                modelPath,
                gpuLayers: config.gpuLayers ?? 0, // 0 = auto-detect
            });
            this.context = new LlamaContext({
                model: this.model,
                contextSize: config.contextSize ?? 4096,
            });
            this.ready = true;
            this.emit('ready', { model: modelPath });
        }
        catch (error) {
            if (error.message.includes('not installed')) {
                throw error;
            }
            throw new Error(`Failed to initialize node-llama-cpp: ${error.message}`);
        }
    }
    isReady() {
        return this.ready;
    }
    async isAvailable() {
        try {
            const nodeLlamaCpp = await import('node-llama-cpp').catch(() => null);
            return nodeLlamaCpp !== null;
        }
        catch {
            return false;
        }
    }
    async complete(messages, options) {
        if (!this.ready || !this.context) {
            throw new Error('Local LLM provider not initialized. Call initialize() with a valid model path first.');
        }
        const startTime = Date.now();
        const nodeLlamaCpp = await import('node-llama-cpp').catch(() => null);
        if (!nodeLlamaCpp) {
            throw new Error('node-llama-cpp is not installed. Install it with: npm install node-llama-cpp');
        }
        const { LlamaChatSession } = nodeLlamaCpp;
        const session = new LlamaChatSession({
            context: this.context,
            systemPrompt: messages.find(m => m.role === 'system')?.content,
        });
        // Build conversation
        let response = '';
        for (const msg of messages) {
            if (msg.role === 'user') {
                response = await session.prompt(msg.content, {
                    maxTokens: options?.maxTokens ?? this.config?.maxTokens ?? 2048,
                    temperature: options?.temperature ?? this.config?.temperature ?? 0.7,
                });
            }
        }
        return {
            content: response,
            tokensUsed: Math.ceil(response.length / 4), // Rough estimate
            model: this.config?.modelPath || 'unknown',
            provider: this.type,
            generationTime: Date.now() - startTime,
        };
    }
    async *stream(messages, options) {
        if (!this.ready || !this.context) {
            throw new Error('Local LLM provider not initialized. Call initialize() with a valid model path first.');
        }
        const nodeLlamaCpp = await import('node-llama-cpp').catch(() => null);
        if (!nodeLlamaCpp) {
            throw new Error('node-llama-cpp is not installed. Install it with: npm install node-llama-cpp');
        }
        const { LlamaChatSession } = nodeLlamaCpp;
        const session = new LlamaChatSession({
            context: this.context,
            systemPrompt: messages.find(m => m.role === 'system')?.content,
        });
        const userMessage = messages.filter(m => m.role === 'user').pop();
        if (!userMessage) {
            throw new Error('No user message found in the conversation. Include at least one message with role "user".');
        }
        // Use streaming API
        const responseIterator = session.prompt(userMessage.content, {
            maxTokens: options?.maxTokens ?? this.config?.maxTokens ?? 2048,
            temperature: options?.temperature ?? this.config?.temperature ?? 0.7,
            onToken: undefined, // We'll use the async iterator instead
        });
        // node-llama-cpp returns the full response, but we can simulate streaming
        // For true streaming, we'd need to use the onToken callback
        const response = await responseIterator;
        // Simulate streaming by yielding chunks
        const chunkSize = 10;
        for (let i = 0; i < response.length; i += chunkSize) {
            yield response.slice(i, i + chunkSize);
            await new Promise(resolve => setTimeout(resolve, 10)); // Small delay for streaming effect
        }
    }
    async getModels() {
        const models = [];
        if (await fs.pathExists(this.modelsDir)) {
            const files = await fs.readdir(this.modelsDir);
            for (const file of files) {
                if (file.endsWith('.gguf')) {
                    models.push(path.join(this.modelsDir, file));
                }
            }
        }
        return models;
    }
    dispose() {
        this.model = null;
        this.context = null;
        this.ready = false;
        this.removeAllListeners();
    }
}
// ============================================================================
// WebLLM Provider
// ============================================================================
/**
 * Browser-based LLM provider using WebLLM
 *
 * Advantages:
 * - Runs in browser with WebGPU
 * - Zero server requirements
 * - Can be used in Electron apps
 * - Progressive model download with caching
 *
 * Limitations:
 * - Requires WebGPU support
 * - Not available in pure Node.js (requires browser/Electron)
 *
 * Usage:
 * ```typescript
 * const provider = new WebLLMProvider();
 * await provider.initialize({ model: 'Llama-3.1-8B-Instruct-q4f16_1-MLC' });
 * const response = await provider.complete([{ role: 'user', content: 'Hello!' }]);
 * ```
 */
export class WebLLMProvider extends EventEmitter {
    type = 'webllm';
    name = 'WebLLM';
    config = null;
    engine = null;
    ready = false;
    async initialize(config) {
        this.config = config;
        try {
            // Dynamic import of WebLLM (optional dependency)
            const webllm = await import('@mlc-ai/web-llm').catch(() => null);
            if (!webllm) {
                throw new ProviderNotAvailableError({
                    provider: 'WebLLM',
                    reason: 'Package not installed. Note: WebLLM requires WebGPU support (browser or Electron)',
                    installCommand: 'npm install @mlc-ai/web-llm',
                });
            }
            const model = config.model || 'Llama-3.1-8B-Instruct-q4f16_1-MLC';
            this.engine = new webllm.MLCEngine();
            // Progress callback
            const initProgress = (progress) => {
                this.emit('progress', progress);
            };
            if (this.engine.reload) {
                await this.engine.reload(model, { initProgressCallback: initProgress });
            }
            this.ready = true;
            this.emit('ready', { model });
        }
        catch (error) {
            if (error.message.includes('not installed')) {
                throw error;
            }
            throw new Error(`Failed to initialize WebLLM: ${error.message}`);
        }
    }
    isReady() {
        return this.ready;
    }
    async isAvailable() {
        try {
            // Check if WebGPU is available
            if (typeof navigator !== 'undefined' && 'gpu' in navigator) {
                const navWithGPU = navigator;
                const adapter = await navWithGPU.gpu?.requestAdapter();
                return adapter !== null;
            }
            return false;
        }
        catch {
            return false;
        }
    }
    async complete(messages, options) {
        if (!this.ready || !this.engine) {
            throw new Error('Local LLM provider not initialized. Call initialize() with a valid model path first.');
        }
        const startTime = Date.now();
        const response = await this.engine.chat.completions.create({
            messages: messages.map(m => ({ role: m.role, content: m.content })),
            max_tokens: options?.maxTokens ?? this.config?.maxTokens ?? 2048,
            temperature: options?.temperature ?? this.config?.temperature ?? 0.7,
            stream: false,
        });
        const content = response.choices[0]?.message?.content || '';
        return {
            content,
            tokensUsed: response.usage?.total_tokens || 0,
            model: this.config?.model || 'unknown',
            provider: this.type,
            generationTime: Date.now() - startTime,
        };
    }
    async *stream(messages, options) {
        if (!this.ready || !this.engine) {
            throw new Error('Local LLM provider not initialized. Call initialize() with a valid model path first.');
        }
        const response = await this.engine.chat.completions.create({
            messages: messages.map(m => ({ role: m.role, content: m.content })),
            max_tokens: options?.maxTokens ?? this.config?.maxTokens ?? 2048,
            temperature: options?.temperature ?? this.config?.temperature ?? 0.7,
            stream: true,
        });
        for await (const chunk of response) {
            const content = chunk.choices[0]?.delta?.content;
            if (content) {
                yield content;
            }
        }
    }
    async getModels() {
        // WebLLM supported models
        return [
            'Llama-3.1-8B-Instruct-q4f16_1-MLC',
            'Llama-3.1-70B-Instruct-q4f16_1-MLC',
            'Mistral-7B-Instruct-v0.3-q4f16_1-MLC',
            'Phi-3.5-mini-instruct-q4f16_1-MLC',
            'Qwen2.5-7B-Instruct-q4f16_1-MLC',
        ];
    }
    dispose() {
        this.engine = null;
        this.ready = false;
        this.removeAllListeners();
    }
}
// ============================================================================
// Ollama Provider (Enhanced)
// ============================================================================
/**
 * Enhanced Ollama provider with better streaming and model management
 *
 * Advantages:
 * - Most mature local LLM solution
 * - Easy model management (pull, list, remove)
 * - Supports many models out of the box
 * - OpenAI-compatible API
 *
 * Usage:
 * ```typescript
 * const provider = new OllamaProvider();
 * await provider.initialize({ model: 'llama3.1', endpoint: 'http://localhost:11434' });
 * const response = await provider.complete([{ role: 'user', content: 'Hello!' }]);
 * ```
 */
export class OllamaProvider extends EventEmitter {
    type = 'ollama';
    name = 'Ollama';
    config = null;
    ready = false;
    endpoint = 'http://localhost:11434';
    async initialize(config) {
        this.config = config;
        this.endpoint = config.endpoint || 'http://localhost:11434';
        // Check if Ollama is running
        const available = await this.isAvailable();
        if (!available) {
            throw new ProviderNotAvailableError({
                provider: 'Ollama',
                reason: 'Server not running or not reachable',
                startCommand: 'ollama serve',
                installCommand: 'Install from https://ollama.com',
            });
        }
        // Check if model is available
        const model = config.model || 'llama3.1';
        const models = await this.getModels();
        if (!models.includes(model)) {
            this.emit('model:pulling', { model });
            await this.pullModel(model);
        }
        this.ready = true;
        this.emit('ready', { model, endpoint: this.endpoint });
    }
    isReady() {
        return this.ready;
    }
    async isAvailable() {
        try {
            const response = await fetch(`${this.endpoint}/api/tags`, {
                method: 'GET',
                signal: AbortSignal.timeout(2000),
            });
            return response.ok;
        }
        catch {
            return false;
        }
    }
    async pullModel(model) {
        const response = await fetch(`${this.endpoint}/api/pull`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ name: model, stream: true }),
        });
        if (!response.ok) {
            throw new Error(`Failed to pull model: ${response.statusText}`);
        }
        const reader = response.body?.getReader();
        if (!reader)
            return;
        const decoder = new TextDecoder();
        const maxIterations = 100000; // Safety limit for model pull progress updates
        let iterations = 0;
        try {
            while (iterations < maxIterations) {
                iterations++;
                const result = await safeStreamRead(reader, {
                    context: 'OllamaPullModel',
                    maxRetries: 2,
                });
                if (!result.success) {
                    handleStreamError(result.error, {
                        source: 'OllamaProvider',
                        operation: 'pulling model',
                        metadata: { model },
                    });
                    throw result.error;
                }
                if (result.done)
                    break;
                const lines = decoder.decode(result.value).split('\n');
                for (const line of lines) {
                    if (line.trim()) {
                        try {
                            const progress = JSON.parse(line);
                            this.emit('progress', progress);
                        }
                        catch {
                            // Skip malformed JSON
                        }
                    }
                }
            }
            if (iterations >= maxIterations) {
                logger.warn(`[OllamaProvider] Pull model reached max iterations: ${maxIterations}`, {
                    source: 'OllamaProvider',
                });
            }
        }
        finally {
            reader.releaseLock();
        }
    }
    async complete(messages, options) {
        if (!this.ready) {
            throw new Error('Local LLM provider not initialized. Call initialize() with a valid model path first.');
        }
        const startTime = Date.now();
        const model = options?.model || this.config?.model || 'llama3.1';
        // Use retry with exponential backoff for Ollama API calls
        const response = await retry(async () => {
            const res = await fetch(`${this.endpoint}/api/chat`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    model,
                    messages: messages.map(m => ({ role: m.role, content: m.content })),
                    stream: false,
                    options: {
                        num_predict: options?.maxTokens ?? this.config?.maxTokens ?? 2048,
                        temperature: options?.temperature ?? this.config?.temperature ?? 0.7,
                    },
                }),
            });
            if (!res.ok) {
                const error = new Error(`Ollama API error: ${res.statusText}`);
                error.status = res.status;
                throw error;
            }
            return res;
        }, {
            ...RetryStrategies.llmApi,
            isRetryable: RetryPredicates.transientError,
            onRetry: (error, attempt, delay) => {
                logger.warn(`Ollama API call failed, retrying (attempt ${attempt}) in ${delay}ms...`, {
                    source: 'OllamaProvider',
                    error: error instanceof Error ? error.message : String(error),
                });
            },
        });
        const data = await response.json();
        return {
            content: data.message.content,
            tokensUsed: data.eval_count || 0,
            model: data.model,
            provider: this.type,
            generationTime: Date.now() - startTime,
        };
    }
    async *stream(messages, options) {
        if (!this.ready) {
            throw new Error('Local LLM provider not initialized. Call initialize() with a valid model path first.');
        }
        const model = options?.model || this.config?.model || 'llama3.1';
        // Use retry with exponential backoff for stream initialization
        const response = await retry(async () => {
            const res = await fetch(`${this.endpoint}/api/chat`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    model,
                    messages: messages.map(m => ({ role: m.role, content: m.content })),
                    stream: true,
                    options: {
                        num_predict: options?.maxTokens ?? this.config?.maxTokens ?? 2048,
                        temperature: options?.temperature ?? this.config?.temperature ?? 0.7,
                    },
                }),
            });
            if (!res.ok) {
                const error = new Error(`Ollama API error: ${res.statusText}`);
                error.status = res.status;
                throw error;
            }
            return res;
        }, {
            ...RetryStrategies.llmApi,
            isRetryable: RetryPredicates.transientError,
            onRetry: (error, attempt, delay) => {
                logger.warn(`Ollama stream initialization failed, retrying (attempt ${attempt}) in ${delay}ms...`, {
                    source: 'OllamaProvider',
                    error: error instanceof Error ? error.message : String(error),
                });
            },
        });
        const reader = response.body?.getReader();
        if (!reader) {
            throw new UserFriendlyError({
                message: 'Ollama returned an empty response body',
                suggestion: 'Check if the Ollama server is running correctly with: ollama list',
                docs: 'https://docs.codebuddy.dev/providers/ollama#troubleshooting',
                context: { endpoint: this.endpoint, model },
            });
        }
        const decoder = new TextDecoder();
        const maxIterations = 50000; // Safety limit for LLM response chunks
        let iterations = 0;
        try {
            while (iterations < maxIterations) {
                iterations++;
                const result = await safeStreamRead(reader, {
                    context: 'OllamaStream',
                    maxRetries: 1,
                });
                if (!result.success) {
                    const errorInfo = handleStreamError(result.error, {
                        source: 'OllamaProvider',
                        operation: 'streaming response',
                        metadata: { model },
                    });
                    // Only throw for non-retryable errors
                    if (!errorInfo.isRetryable) {
                        throw result.error;
                    }
                    // For retryable errors, log and continue (reader may recover)
                    continue;
                }
                if (result.done)
                    break;
                const lines = decoder.decode(result.value).split('\n');
                for (const line of lines) {
                    if (line.trim()) {
                        try {
                            const data = JSON.parse(line);
                            if (data.message?.content) {
                                yield data.message.content;
                            }
                        }
                        catch {
                            // Skip malformed JSON
                        }
                    }
                }
            }
            if (iterations >= maxIterations) {
                logger.warn(`[OllamaProvider] Stream reached max iterations: ${maxIterations}`, {
                    source: 'OllamaProvider',
                });
            }
        }
        finally {
            reader.releaseLock();
        }
    }
    async getModels() {
        try {
            const response = await fetch(`${this.endpoint}/api/tags`, {
                method: 'GET',
            });
            if (!response.ok) {
                return [];
            }
            const data = await response.json();
            return data.models?.map(m => m.name) || [];
        }
        catch {
            return [];
        }
    }
    dispose() {
        this.ready = false;
        this.removeAllListeners();
    }
}
// ============================================================================
// Local Provider Manager
// ============================================================================
/**
 * Manager for local LLM providers
 *
 * Handles provider selection, fallback, and unified interface.
 */
export class LocalProviderManager extends EventEmitter {
    providers = new Map();
    activeProvider = null;
    /**
     * Register and initialize a provider
     */
    async registerProvider(type, config) {
        const provider = this.createProvider(type);
        provider.on('progress', (progress) => {
            this.emit('progress', { provider: type, ...progress });
        });
        provider.on('ready', (info) => {
            this.emit('provider:ready', { type, ...info });
        });
        await provider.initialize(config);
        this.providers.set(type, provider);
        if (!this.activeProvider) {
            this.activeProvider = type;
        }
    }
    /**
     * Create provider instance
     */
    createProvider(type) {
        switch (type) {
            case 'ollama':
                return new OllamaProvider();
            case 'local-llama':
                return new NodeLlamaCppProvider();
            case 'webllm':
                return new WebLLMProvider();
            default:
                throw new Error(`Unknown local provider type: ${type}`);
        }
    }
    /**
     * Get active provider
     */
    getActiveProvider() {
        if (!this.activeProvider)
            return null;
        return this.providers.get(this.activeProvider) || null;
    }
    /**
     * Set active provider
     */
    setActiveProvider(type) {
        if (!this.providers.has(type)) {
            throw new Error(`Provider ${type} not registered`);
        }
        this.activeProvider = type;
        this.emit('provider:changed', { type });
    }
    /**
     * Auto-detect best available provider
     */
    async autoDetectProvider() {
        // Priority: Ollama > node-llama-cpp > WebLLM
        // Check Ollama first (most common)
        const ollama = new OllamaProvider();
        if (await ollama.isAvailable()) {
            return 'ollama';
        }
        // Check node-llama-cpp
        const nodeLlama = new NodeLlamaCppProvider();
        if (await nodeLlama.isAvailable()) {
            return 'local-llama';
        }
        // Check WebLLM (requires browser/Electron)
        const webllm = new WebLLMProvider();
        if (await webllm.isAvailable()) {
            return 'webllm';
        }
        return null;
    }
    /**
     * Complete with active provider (with fallback)
     */
    async complete(messages, options) {
        const provider = this.getActiveProvider();
        if (!provider) {
            throw new Error('No local provider available');
        }
        try {
            return await provider.complete(messages, options);
        }
        catch (error) {
            // Try fallback providers
            for (const [type, fallbackProvider] of this.providers) {
                if (type !== this.activeProvider && fallbackProvider.isReady()) {
                    this.emit('provider:fallback', { from: this.activeProvider, to: type, error });
                    return await fallbackProvider.complete(messages, options);
                }
            }
            throw error;
        }
    }
    /**
     * Stream with active provider
     */
    stream(messages, options) {
        const provider = this.getActiveProvider();
        if (!provider) {
            throw new Error('No local provider available');
        }
        return provider.stream(messages, options);
    }
    /**
     * Get all registered providers
     */
    getRegisteredProviders() {
        return Array.from(this.providers.keys());
    }
    /**
     * Dispose all providers
     */
    dispose() {
        for (const provider of this.providers.values()) {
            provider.dispose();
        }
        this.providers.clear();
        this.activeProvider = null;
        this.removeAllListeners();
    }
}
// ============================================================================
// Singleton
// ============================================================================
let localProviderManagerInstance = null;
export function getLocalProviderManager() {
    if (!localProviderManagerInstance) {
        localProviderManagerInstance = new LocalProviderManager();
    }
    return localProviderManagerInstance;
}
export function resetLocalProviderManager() {
    if (localProviderManagerInstance) {
        localProviderManagerInstance.dispose();
    }
    localProviderManagerInstance = null;
}
// ============================================================================
// Auto-configuration
// ============================================================================
/**
 * Auto-configure best available local provider
 */
export async function autoConfigureLocalProvider(preferredProvider) {
    const manager = getLocalProviderManager();
    // Try preferred provider first
    if (preferredProvider) {
        try {
            await manager.registerProvider(preferredProvider, {});
            return manager;
        }
        catch (error) {
            logger.warn(`Preferred provider ${preferredProvider} not available:`, { source: 'LocalProviders', error });
        }
    }
    // Auto-detect
    const detected = await manager.autoDetectProvider();
    if (detected) {
        await manager.registerProvider(detected, {});
        return manager;
    }
    throw new Error('No local LLM provider available.\n' +
        'Options:\n' +
        '1. Install Ollama: https://ollama.com\n' +
        '2. Install node-llama-cpp: npm install node-llama-cpp\n' +
        '3. Use WebLLM in browser/Electron environment');
}
//# sourceMappingURL=local-llm-provider.js.map