/**
 * Offline Mode System
 *
 * Features:
 * - Response caching for offline use
 * - Local LLM fallback (Ollama, llama.cpp)
 * - Embedding cache for semantic search
 * - Queue requests when offline
 * - Automatic sync when back online
 * - Offline-capable tools
 *
 * Allows Grok CLI to function without internet connectivity.
 */
import { EventEmitter } from 'events';
import fs from 'fs-extra';
import * as path from 'path';
import * as os from 'os';
import * as crypto from 'crypto';
import axios from 'axios';
import { LRUCache } from '../utils/lru-cache.js';
import { autoConfigureLocalProvider, } from '../providers/local-llm-provider.js';
const DEFAULT_CONFIG = {
    enabled: true,
    cacheEnabled: true,
    cacheMaxSize: 500, // 500 MB
    cacheMaxAge: 30, // 30 days
    localLLMEnabled: true,
    localLLMProvider: 'ollama',
    localLLMModel: 'llama3.2',
    embeddingCacheEnabled: true,
    queueRequestsWhenOffline: true,
    autoSyncOnReconnect: true,
    checkInternetInterval: 30000, // 30 seconds
};
/**
 * Offline Mode Manager
 */
// Cache size limits to prevent memory leaks
const MAX_RESPONSE_CACHE_SIZE = 500;
const MAX_EMBEDDING_CACHE_SIZE = 1000;
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
export class OfflineMode extends EventEmitter {
    config;
    dataDir;
    cacheDir;
    // Use LRU cache with size limits to prevent memory leaks
    responseCache;
    embeddingCache;
    requestQueue = [];
    isOnline = true;
    lastOnline = new Date();
    stats;
    checkInternetTimer = null;
    localLLMProcess = null;
    localProviderManager = null;
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_CONFIG, ...config };
        this.dataDir = path.join(os.homedir(), '.codebuddy', 'offline');
        this.cacheDir = path.join(this.dataDir, 'cache');
        // Initialize LRU caches with size limits
        this.responseCache = new LRUCache({
            maxSize: MAX_RESPONSE_CACHE_SIZE,
            ttlMs: CACHE_TTL_MS,
            onEvict: (key, value) => {
                this.emit('cache:evict', { type: 'response', key, query: value.query });
            },
        });
        this.embeddingCache = new LRUCache({
            maxSize: MAX_EMBEDDING_CACHE_SIZE,
            ttlMs: CACHE_TTL_MS,
            onEvict: (key) => {
                this.emit('cache:evict', { type: 'embedding', key });
            },
        });
        this.stats = {
            cacheHits: 0,
            cacheMisses: 0,
            localLLMCalls: 0,
            queuedRequests: 0,
            cacheSize: 0,
            isOnline: true,
        };
        this.initialize();
    }
    /**
     * Initialize offline mode
     */
    async initialize() {
        await fs.ensureDir(this.dataDir);
        await fs.ensureDir(this.cacheDir);
        await fs.ensureDir(path.join(this.cacheDir, 'responses'));
        await fs.ensureDir(path.join(this.cacheDir, 'embeddings'));
        await this.loadCaches();
        await this.loadQueue();
        this.isOnline = await this.checkInternet();
        this.startInternetCheck();
    }
    /**
     * Load caches from disk
     */
    async loadCaches() {
        // Load response cache index
        const indexPath = path.join(this.cacheDir, 'response-index.json');
        if (await fs.pathExists(indexPath)) {
            try {
                const index = await fs.readJSON(indexPath);
                for (const item of index) {
                    this.responseCache.set(item.queryHash, item);
                }
            }
            catch {
                // Start fresh
            }
        }
        // Load embedding cache index
        const embIndexPath = path.join(this.cacheDir, 'embedding-index.json');
        if (await fs.pathExists(embIndexPath)) {
            try {
                const index = await fs.readJSON(embIndexPath);
                for (const item of index) {
                    this.embeddingCache.set(item.textHash, item);
                }
            }
            catch {
                // Start fresh
            }
        }
        // Calculate cache size
        await this.calculateCacheSize();
    }
    /**
     * Load request queue
     */
    async loadQueue() {
        const queuePath = path.join(this.dataDir, 'queue.json');
        if (await fs.pathExists(queuePath)) {
            try {
                this.requestQueue = await fs.readJSON(queuePath);
                this.stats.queuedRequests = this.requestQueue.length;
            }
            catch {
                this.requestQueue = [];
            }
        }
    }
    /**
     * Save request queue
     */
    async saveQueue() {
        const queuePath = path.join(this.dataDir, 'queue.json');
        await fs.writeJSON(queuePath, this.requestQueue, { spaces: 2 });
    }
    /**
     * Save cache indexes
     */
    async saveCacheIndexes() {
        await fs.writeJSON(path.join(this.cacheDir, 'response-index.json'), Array.from(this.responseCache.values()), { spaces: 2 });
        await fs.writeJSON(path.join(this.cacheDir, 'embedding-index.json'), Array.from(this.embeddingCache.values()), { spaces: 2 });
    }
    /**
     * Calculate cache size
     */
    async calculateCacheSize() {
        let size = 0;
        const walk = async (dir) => {
            const entries = await fs.readdir(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(dir, entry.name);
                if (entry.isDirectory()) {
                    await walk(fullPath);
                }
                else {
                    const stat = await fs.stat(fullPath);
                    size += stat.size;
                }
            }
        };
        await walk(this.cacheDir);
        this.stats.cacheSize = size;
    }
    /**
     * Check internet connectivity
     */
    async checkInternet() {
        try {
            await axios.get('https://api.x.ai/health', {
                timeout: 5000,
            });
            return true;
        }
        catch {
            try {
                // Fallback check
                await axios.get('https://www.google.com/generate_204', {
                    timeout: 5000,
                });
                return true;
            }
            catch {
                return false;
            }
        }
    }
    /**
     * Start internet connectivity checker
     */
    startInternetCheck() {
        this.checkInternetTimer = setInterval(async () => {
            const wasOnline = this.isOnline;
            this.isOnline = await this.checkInternet();
            this.stats.isOnline = this.isOnline;
            if (!wasOnline && this.isOnline) {
                this.lastOnline = new Date();
                this.stats.lastOnline = this.lastOnline;
                this.emit('online');
                if (this.config.autoSyncOnReconnect) {
                    this.processQueue();
                }
            }
            else if (wasOnline && !this.isOnline) {
                this.emit('offline');
            }
        }, this.config.checkInternetInterval);
    }
    /**
     * Hash a string for cache lookup
     */
    hash(str) {
        return crypto.createHash('sha256').update(str).digest('hex');
    }
    /**
     * Cache a response
     */
    async cacheResponse(query, response, model, tokensUsed) {
        if (!this.config.cacheEnabled)
            return;
        const queryHash = this.hash(query);
        const id = crypto.randomBytes(8).toString('hex');
        const cached = {
            id,
            query,
            queryHash,
            response,
            model,
            tokensUsed,
            createdAt: new Date(),
            accessedAt: new Date(),
            accessCount: 1,
        };
        // Save response to file
        const responsePath = path.join(this.cacheDir, 'responses', `${id}.json`);
        await fs.writeJSON(responsePath, cached, { spaces: 2 });
        this.responseCache.set(queryHash, cached);
        await this.saveCacheIndexes();
        // Check cache size and cleanup if needed
        await this.cleanupCacheIfNeeded();
    }
    /**
     * Get cached response
     */
    async getCachedResponse(query) {
        if (!this.config.cacheEnabled)
            return null;
        const queryHash = this.hash(query);
        const cached = this.responseCache.get(queryHash);
        if (cached) {
            // Check if expired
            const age = Date.now() - new Date(cached.createdAt).getTime();
            const maxAge = this.config.cacheMaxAge * 24 * 60 * 60 * 1000;
            if (age > maxAge) {
                await this.removeCachedResponse(queryHash);
                this.stats.cacheMisses++;
                return null;
            }
            // Update access stats
            cached.accessedAt = new Date();
            cached.accessCount++;
            this.stats.cacheHits++;
            return cached;
        }
        this.stats.cacheMisses++;
        return null;
    }
    /**
     * Find similar cached responses using embedding similarity
     */
    async findSimilarResponses(query, threshold = 0.85) {
        if (!this.config.embeddingCacheEnabled)
            return [];
        // Get or compute query embedding
        const queryEmbedding = await this.getEmbedding(query);
        if (!queryEmbedding)
            return [];
        const similar = [];
        for (const cached of this.responseCache.values()) {
            // Get cached query embedding
            const cachedEmbedding = this.embeddingCache.get(this.hash(cached.query));
            if (cachedEmbedding) {
                const similarity = this.cosineSimilarity(queryEmbedding, cachedEmbedding.embedding);
                if (similarity >= threshold) {
                    similar.push({ ...cached, similarity });
                }
            }
        }
        return similar.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
    }
    /**
     * Cosine similarity between two vectors
     */
    cosineSimilarity(a, b) {
        if (a.length !== b.length)
            return 0;
        let dotProduct = 0;
        let normA = 0;
        let normB = 0;
        for (let i = 0; i < a.length; i++) {
            dotProduct += a[i] * b[i];
            normA += a[i] * a[i];
            normB += b[i] * b[i];
        }
        return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
    }
    /**
     * Cache an embedding
     */
    async cacheEmbedding(text, embedding, model) {
        if (!this.config.embeddingCacheEnabled)
            return;
        const textHash = this.hash(text);
        const id = crypto.randomBytes(8).toString('hex');
        const cached = {
            id,
            text,
            textHash,
            embedding,
            model,
            createdAt: new Date(),
        };
        // Save embedding to file
        const embPath = path.join(this.cacheDir, 'embeddings', `${id}.json`);
        await fs.writeJSON(embPath, cached);
        this.embeddingCache.set(textHash, cached);
        await this.saveCacheIndexes();
    }
    /**
     * Get cached embedding
     */
    async getEmbedding(text) {
        const textHash = this.hash(text);
        const cached = this.embeddingCache.get(textHash);
        return cached?.embedding || null;
    }
    /**
     * Remove cached response
     */
    async removeCachedResponse(queryHash) {
        const cached = this.responseCache.get(queryHash);
        if (cached) {
            const responsePath = path.join(this.cacheDir, 'responses', `${cached.id}.json`);
            await fs.remove(responsePath);
            this.responseCache.delete(queryHash);
        }
    }
    /**
     * Cleanup cache if needed
     */
    async cleanupCacheIfNeeded() {
        await this.calculateCacheSize();
        const maxSizeBytes = this.config.cacheMaxSize * 1024 * 1024;
        if (this.stats.cacheSize <= maxSizeBytes)
            return;
        // Sort by access time (LRU)
        const sorted = Array.from(this.responseCache.values())
            .sort((a, b) => new Date(a.accessedAt).getTime() - new Date(b.accessedAt).getTime());
        // Remove oldest until under limit
        for (const cached of sorted) {
            await this.removeCachedResponse(cached.queryHash);
            await this.calculateCacheSize();
            if (this.stats.cacheSize <= maxSizeBytes * 0.8) {
                break;
            }
        }
        await this.saveCacheIndexes();
        this.emit('cache:cleaned');
    }
    /**
     * Queue a request for later processing
     */
    queueRequest(type, payload, priority = 0) {
        if (!this.config.queueRequestsWhenOffline) {
            throw new Error('Request queuing is disabled');
        }
        const request = {
            id: crypto.randomBytes(8).toString('hex'),
            type,
            payload,
            createdAt: new Date(),
            retries: 0,
            priority,
        };
        this.requestQueue.push(request);
        this.requestQueue.sort((a, b) => b.priority - a.priority);
        this.stats.queuedRequests = this.requestQueue.length;
        this.saveQueue();
        this.emit('request:queued', { request });
        return request.id;
    }
    /**
     * Process queued requests
     */
    async processQueue() {
        if (!this.isOnline)
            return;
        this.emit('queue:processing');
        const toProcess = [...this.requestQueue];
        this.requestQueue = [];
        for (const request of toProcess) {
            try {
                await this.processRequest(request);
                this.emit('request:processed', { request });
            }
            catch (error) {
                request.retries++;
                if (request.retries < 3) {
                    this.requestQueue.push(request);
                }
                else {
                    this.emit('request:failed', { request, error });
                }
            }
        }
        this.stats.queuedRequests = this.requestQueue.length;
        await this.saveQueue();
        this.emit('queue:processed');
    }
    /**
     * Process a single request
     */
    async processRequest(request) {
        // This would integrate with the main Grok client
        // For now, just emit an event
        this.emit('request:execute', { request });
        return null;
    }
    /**
     * Call local LLM using the new provider system
     */
    async callLocalLLM(prompt, options = {}) {
        if (!this.config.localLLMEnabled)
            return null;
        const model = options.model || this.config.localLLMModel;
        try {
            // Use new provider system for local-llama and webllm
            if (this.config.localLLMProvider === 'local-llama' || this.config.localLLMProvider === 'webllm') {
                return await this.callNewProvider(prompt, model, options);
            }
            // Legacy provider support
            switch (this.config.localLLMProvider) {
                case 'ollama':
                    return this.callOllama(prompt, model, options);
                case 'llamacpp':
                    return this.callLlamaCpp(prompt, model, options);
                default:
                    return null;
            }
        }
        catch (error) {
            this.emit('localLLM:error', { error });
            return null;
        }
    }
    /**
     * Call new provider system (node-llama-cpp, WebLLM)
     */
    async callNewProvider(prompt, model, options) {
        // Initialize provider manager if needed
        if (!this.localProviderManager) {
            const providerType = this.config.localLLMProvider;
            try {
                this.localProviderManager = await autoConfigureLocalProvider(providerType);
                this.localProviderManager.on('progress', (progress) => {
                    this.emit('localLLM:progress', progress);
                });
            }
            catch (error) {
                this.emit('localLLM:error', { error });
                return null;
            }
        }
        const messages = [
            { role: 'user', content: prompt }
        ];
        const response = await this.localProviderManager.complete(messages, {
            model,
            maxTokens: options.maxTokens || 2048,
            temperature: options.temperature || 0.7,
            modelPath: this.config.localLLMModelPath,
            gpuLayers: this.config.localLLMGpuLayers,
        });
        this.stats.localLLMCalls++;
        return response.content;
    }
    /**
     * Stream local LLM response (new provider system)
     */
    async *streamLocalLLM(prompt, options = {}) {
        if (!this.config.localLLMEnabled)
            return;
        // Initialize provider manager if needed
        if (!this.localProviderManager) {
            const providerType = this.config.localLLMProvider;
            if (providerType === 'local-llama' || providerType === 'webllm' || providerType === 'ollama') {
                try {
                    this.localProviderManager = await autoConfigureLocalProvider(providerType);
                }
                catch (error) {
                    this.emit('localLLM:error', { error });
                    return;
                }
            }
            else {
                // Fallback to non-streaming for legacy providers
                const result = await this.callLocalLLM(prompt, options);
                if (result)
                    yield result;
                return;
            }
        }
        const messages = [
            { role: 'user', content: prompt }
        ];
        const stream = this.localProviderManager.stream(messages, {
            model: options.model || this.config.localLLMModel,
            maxTokens: options.maxTokens || 2048,
            temperature: options.temperature || 0.7,
        });
        for await (const chunk of stream) {
            yield chunk;
        }
        this.stats.localLLMCalls++;
    }
    /**
     * Call Ollama API
     */
    async callOllama(prompt, model, options) {
        const endpoint = this.config.localLLMEndpoint || 'http://localhost:11434';
        const response = await axios.post(`${endpoint}/api/generate`, {
            model,
            prompt,
            stream: false,
            options: {
                num_predict: options.maxTokens || 2048,
                temperature: options.temperature || 0.7,
            },
        }, {
            timeout: 60000,
        });
        this.stats.localLLMCalls++;
        return response.data.response;
    }
    /**
     * Call llama.cpp server
     */
    async callLlamaCpp(prompt, model, options) {
        const endpoint = this.config.localLLMEndpoint || 'http://localhost:8080';
        const response = await axios.post(`${endpoint}/completion`, {
            prompt,
            n_predict: options.maxTokens || 2048,
            temperature: options.temperature || 0.7,
        }, {
            timeout: 60000,
        });
        this.stats.localLLMCalls++;
        return response.data.content;
    }
    /**
     * Check if local LLM is available
     */
    async isLocalLLMAvailable() {
        if (!this.config.localLLMEnabled)
            return false;
        try {
            switch (this.config.localLLMProvider) {
                case 'ollama': {
                    const endpoint = this.config.localLLMEndpoint || 'http://localhost:11434';
                    await axios.get(`${endpoint}/api/tags`, { timeout: 2000 });
                    return true;
                }
                case 'llamacpp': {
                    const endpoint = this.config.localLLMEndpoint || 'http://localhost:8080';
                    await axios.get(`${endpoint}/health`, { timeout: 2000 });
                    return true;
                }
                case 'local-llama': {
                    // Check if node-llama-cpp is available
                    try {
                        const nodeLlamaCpp = await import('node-llama-cpp').catch(() => null);
                        return nodeLlamaCpp !== null;
                    }
                    catch {
                        return false;
                    }
                }
                case 'webllm': {
                    // Check if WebGPU is available (browser/Electron only)
                    if (typeof navigator !== 'undefined' && 'gpu' in navigator) {
                        const adapter = await navigator.gpu.requestAdapter();
                        return adapter !== null;
                    }
                    return false;
                }
                default:
                    return false;
            }
        }
        catch {
            return false;
        }
    }
    /**
     * Get provider manager instance
     */
    getLocalProviderManager() {
        return this.localProviderManager;
    }
    /**
     * Get available provider types
     */
    async getAvailableProviders() {
        const available = [];
        // Check Ollama
        try {
            const endpoint = this.config.localLLMEndpoint || 'http://localhost:11434';
            await axios.get(`${endpoint}/api/tags`, { timeout: 2000 });
            available.push('ollama');
        }
        catch {
            // Not available
        }
        // Check llama.cpp HTTP server
        try {
            const endpoint = this.config.localLLMEndpoint || 'http://localhost:8080';
            await axios.get(`${endpoint}/health`, { timeout: 2000 });
            available.push('llamacpp');
        }
        catch {
            // Not available
        }
        // Check node-llama-cpp
        try {
            const nodeLlamaCpp = await import('node-llama-cpp').catch(() => null);
            if (nodeLlamaCpp) {
                available.push('local-llama');
            }
        }
        catch {
            // Not available
        }
        // Check WebLLM (only in browser/Electron)
        if (typeof navigator !== 'undefined' && 'gpu' in navigator) {
            try {
                const adapter = await navigator.gpu.requestAdapter();
                if (adapter) {
                    available.push('webllm');
                }
            }
            catch {
                // Not available
            }
        }
        return available;
    }
    /**
     * Get available local models
     */
    async getLocalModels() {
        try {
            if (this.config.localLLMProvider === 'ollama') {
                const endpoint = this.config.localLLMEndpoint || 'http://localhost:11434';
                const response = await axios.get(`${endpoint}/api/tags`, { timeout: 5000 });
                return response.data.models?.map((m) => m.name) || [];
            }
            return [];
        }
        catch {
            return [];
        }
    }
    /**
     * Get stats
     */
    getStats() {
        return { ...this.stats, isOnline: this.isOnline };
    }
    /**
     * Get config
     */
    getConfig() {
        return { ...this.config };
    }
    /**
     * Update config
     */
    updateConfig(config) {
        this.config = { ...this.config, ...config };
        this.saveConfig();
    }
    /**
     * Save config
     */
    async saveConfig() {
        const configPath = path.join(this.dataDir, 'config.json');
        await fs.writeJSON(configPath, this.config, { spaces: 2 });
    }
    /**
     * Clear cache
     */
    async clearCache() {
        await fs.emptyDir(path.join(this.cacheDir, 'responses'));
        await fs.emptyDir(path.join(this.cacheDir, 'embeddings'));
        this.responseCache.clear();
        this.embeddingCache.clear();
        await this.saveCacheIndexes();
        await this.calculateCacheSize();
        this.emit('cache:cleared');
    }
    /**
     * Clear queue
     */
    async clearQueue() {
        this.requestQueue = [];
        this.stats.queuedRequests = 0;
        await this.saveQueue();
        this.emit('queue:cleared');
    }
    /**
     * Format status
     */
    formatStatus() {
        const stats = this.getStats();
        const cacheHitRate = stats.cacheHits + stats.cacheMisses > 0
            ? ((stats.cacheHits / (stats.cacheHits + stats.cacheMisses)) * 100).toFixed(1)
            : '0.0';
        const providerName = this.config.localLLMProvider === 'local-llama' ? 'node-llama-cpp'
            : this.config.localLLMProvider === 'webllm' ? 'WebLLM'
                : this.config.localLLMProvider;
        const lines = [
            '╔══════════════════════════════════════════════════════════════╗',
            '║                     📴 OFFLINE MODE                          ║',
            '╠══════════════════════════════════════════════════════════════╣',
            `║ Status:          ${stats.isOnline ? '🟢 Online' : '🔴 Offline'}                              ║`,
            `║ Cache Enabled:   ${this.config.cacheEnabled ? '✅' : '❌'}                                       ║`,
            `║ Local LLM:       ${this.config.localLLMEnabled ? '✅ ' + providerName : '❌ Disabled'}                    ║`,
            `║ Model:           ${this.config.localLLMModel.substring(0, 20)}                       ║`,
            '╠══════════════════════════════════════════════════════════════╣',
            '║ CACHE STATS                                                  ║',
            `║ Size:            ${(stats.cacheSize / 1024 / 1024).toFixed(1)} MB / ${this.config.cacheMaxSize} MB                        ║`,
            `║ Responses:       ${this.responseCache.size}                                          ║`,
            `║ Embeddings:      ${this.embeddingCache.size}                                          ║`,
            `║ Hit Rate:        ${cacheHitRate}%                                        ║`,
            '╠══════════════════════════════════════════════════════════════╣',
            `║ Queued Requests: ${stats.queuedRequests}                                           ║`,
            `║ Local LLM Calls: ${stats.localLLMCalls}                                           ║`,
            '╠══════════════════════════════════════════════════════════════╣',
            '║ SUPPORTED PROVIDERS                                          ║',
            '║ • ollama      - Ollama API (localhost:11434)                 ║',
            '║ • llamacpp    - llama.cpp HTTP server (localhost:8080)       ║',
            '║ • local-llama - node-llama-cpp (native bindings)             ║',
            '║ • webllm      - WebLLM (WebGPU browser/Electron)             ║',
            '╠══════════════════════════════════════════════════════════════╣',
            '║ /offline cache clear | /offline queue process                ║',
            '╚══════════════════════════════════════════════════════════════╝',
        ];
        return lines.join('\n');
    }
    /**
     * Dispose
     */
    dispose() {
        if (this.checkInternetTimer) {
            clearInterval(this.checkInternetTimer);
        }
        if (this.localLLMProcess) {
            this.localLLMProcess.kill();
        }
        if (this.localProviderManager) {
            this.localProviderManager.dispose();
            this.localProviderManager = null;
        }
        this.saveCacheIndexes();
        this.saveQueue();
        this.removeAllListeners();
    }
}
// Singleton
let offlineModeInstance = null;
export function getOfflineMode(config) {
    if (!offlineModeInstance) {
        offlineModeInstance = new OfflineMode(config);
    }
    return offlineModeInstance;
}
export function resetOfflineMode() {
    if (offlineModeInstance) {
        offlineModeInstance.dispose();
    }
    offlineModeInstance = null;
}
//# sourceMappingURL=offline-mode.js.map