/**
 * Vector embeddings utilities using OpenAI
 */
import OpenAI from 'openai';
import { encoding_for_model } from 'tiktoken';
import { UsageTrackingDB } from '../database/usage-tracking.js';
export class EmbeddingService {
    openai;
    model;
    usageTracker = null;
    db = null;
    // Pricing constants (per 1M tokens)
    EMBEDDING_PRICE_PER_1M = 0.02; // text-embedding-3-small
    constructor(apiKey, model = 'text-embedding-3-small', db) {
        const key = apiKey || process.env.OPENAI_API_KEY;
        if (key && key !== 'test-key') {
            this.openai = new OpenAI({
                apiKey: key,
            });
        }
        else {
            this.openai = null;
            console.warn('OpenAI API key not provided - embeddings will be disabled');
        }
        this.model = model;
        if (db) {
            this.db = db;
            this.usageTracker = new UsageTrackingDB(db);
            console.error('[EmbeddingService] Usage tracking enabled');
        }
    }
    /**
     * Generate embeddings for text
     */
    async generateEmbedding(text, userId) {
        if (!this.openai) {
            console.warn('[EmbeddingService] ⚠️  OpenAI client not initialized - skipping embedding generation');
            console.warn('[EmbeddingService] 💡 Set OPENAI_API_KEY environment variable to enable embeddings');
            return [];
        }
        if (!text || text.trim().length === 0) {
            console.warn('[EmbeddingService] ⚠️  Empty text provided - cannot generate embedding');
            return [];
        }
        try {
            const trimmedText = text.trim();
            const startTime = Date.now();
            console.error(`[EmbeddingService] 🔄 Generating embedding for text (${trimmedText.length} chars)...`);
            const response = await this.openai.embeddings.create({
                model: this.model,
                input: trimmedText,
            });
            const embedding = response.data[0]?.embedding;
            if (!embedding || !Array.isArray(embedding) || embedding.length === 0) {
                console.error('[EmbeddingService] ❌ OpenAI returned invalid embedding:', {
                    hasEmbedding: !!embedding,
                    isArray: Array.isArray(embedding),
                    length: embedding?.length || 0
                });
                throw new Error('OpenAI API returned invalid or empty embedding');
            }
            // Validate expected dimensions for text-embedding-3-small (1536)
            if (embedding.length !== 1536) {
                console.warn(`[EmbeddingService] ⚠️  Unexpected embedding dimensions: ${embedding.length} (expected 1536 for ${this.model})`);
            }
            // Validate that all values are numbers
            const hasInvalidValues = embedding.some(val => typeof val !== 'number' || !isFinite(val));
            if (hasInvalidValues) {
                console.error('[EmbeddingService] ❌ Embedding contains invalid values (non-numeric or infinite)');
                throw new Error('Generated embedding contains invalid values');
            }
            const tokensUsed = response.usage?.total_tokens || 0;
            const cost = this.calculateCost(tokensUsed);
            const duration = Date.now() - startTime;
            console.error(`[EmbeddingService] ✅ Successfully generated embedding: ${embedding.length} dimensions, ` +
                `${tokensUsed} tokens, $${cost.toFixed(6)}, ${duration}ms`);
            // Track usage if tracker is available and userId is provided
            if (this.usageTracker && userId) {
                await this.recordUsage({
                    userId,
                    tokensUsed,
                    cost,
                    duration
                }).catch(err => {
                    console.error('[EmbeddingService] ⚠️  Failed to record usage:', err);
                });
            }
            return embedding;
        }
        catch (error) {
            console.error('[EmbeddingService] ❌ Failed to generate embedding:', error);
            // Add more specific error messages
            if (error instanceof Error) {
                if (error.message.includes('API key')) {
                    console.error('[EmbeddingService] 🔑 API Key issue - check OPENAI_API_KEY environment variable');
                }
                else if (error.message.includes('rate limit')) {
                    console.error('[EmbeddingService] ⏱️  Rate limit exceeded - please wait and retry');
                }
                else if (error.message.includes('timeout')) {
                    console.error('[EmbeddingService] ⏱️  Request timeout - network or API issues');
                }
            }
            throw new Error(`Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    /**
     * Generate embeddings for multiple texts
     */
    async generateEmbeddings(texts, userId) {
        if (!this.openai) {
            // Return empty arrays when OpenAI is not available
            return texts.map(() => []);
        }
        try {
            const validTexts = texts.filter(t => t && t.trim().length > 0);
            if (validTexts.length === 0) {
                return texts.map(() => []);
            }
            const startTime = Date.now();
            const response = await this.openai.embeddings.create({
                model: this.model,
                input: validTexts.map(text => text.trim()),
            });
            const embeddings = response.data.map(item => item.embedding);
            const tokensUsed = response.usage?.total_tokens || 0;
            const cost = this.calculateCost(tokensUsed);
            const duration = Date.now() - startTime;
            console.error(`[EmbeddingService] ✅ Generated ${embeddings.length} embeddings: ` +
                `${tokensUsed} tokens, $${cost.toFixed(6)}, ${duration}ms`);
            // Track usage if tracker is available and userId is provided
            if (this.usageTracker && userId) {
                await this.recordUsage({
                    userId,
                    tokensUsed,
                    cost,
                    duration,
                    batchSize: validTexts.length
                }).catch(err => {
                    console.error('[EmbeddingService] ⚠️  Failed to record usage:', err);
                });
            }
            return embeddings;
        }
        catch (error) {
            console.error('Failed to generate embeddings:', error);
            throw new Error(`Batch embedding generation failed: ${error}`);
        }
    }
    /**
     * Calculate cosine similarity between two vectors
     */
    static cosineSimilarity(a, b) {
        if (!Array.isArray(a) || !Array.isArray(b)) {
            console.error('[CosineSimilarity] Invalid input: not arrays', typeof a, typeof b);
            return 0;
        }
        if (a.length !== b.length) {
            console.error('[CosineSimilarity] Vector length mismatch:', a.length, 'vs', b.length);
            return 0;
        }
        if (a.length === 0) {
            return 0;
        }
        let dotProduct = 0;
        let normA = 0;
        let normB = 0;
        for (let i = 0; i < a.length; i++) {
            const valA = a[i];
            const valB = b[i];
            // Validate that values are numbers
            if (typeof valA !== 'number' || typeof valB !== 'number') {
                console.error('[CosineSimilarity] Non-numeric values detected at index', i);
                return 0;
            }
            dotProduct += valA * valB;
            normA += valA * valA;
            normB += valB * valB;
        }
        if (normA === 0 || normB === 0) {
            if (process.env.MCP_DEBUG) {
                console.error('[CosineSimilarity] Zero norm detected:', normA, normB);
            }
            return 0;
        }
        const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
        // OpenAI embeddings are normalized, so similarity should be between -1 and 1
        // In practice, semantic similarity is usually between 0 and 1
        if (similarity < -1.1 || similarity > 1.1) {
            console.warn('[CosineSimilarity] Unusual similarity value:', similarity);
        }
        return similarity;
    }
    /**
     * Find the most similar vectors from a list
     */
    static findMostSimilar(queryVector, vectors, threshold = 0.7, limit = 10) {
        const similarities = vectors
            .map(item => ({
            similarity: EmbeddingService.cosineSimilarity(queryVector, item.vector),
            data: item.data,
        }))
            .filter(item => item.similarity >= threshold)
            .sort((a, b) => b.similarity - a.similarity)
            .slice(0, limit);
        return similarities;
    }
    /**
     * Prepare text for embedding (clean and truncate)
     */
    static prepareTextForEmbedding(text, maxLength = 8000) {
        // Clean the text
        let cleaned = text
            .replace(/\s+/g, ' ') // Normalize whitespace
            .replace(/[^\w\s\-.,!?;:()\[\]{}'"]/g, '') // Remove special chars
            .trim();
        // Truncate if too long
        if (cleaned.length > maxLength) {
            cleaned = cleaned.substring(0, maxLength).trim();
            // Try to end at a word boundary
            const lastSpace = cleaned.lastIndexOf(' ');
            if (lastSpace > maxLength * 0.8) {
                cleaned = cleaned.substring(0, lastSpace);
            }
        }
        return cleaned;
    }
    /**
     * Create a combined text for embedding from memory data
     */
    static createMemoryEmbeddingText(memory) {
        const parts = [
            memory.title,
            memory.content,
        ];
        if (memory.memoryType) {
            parts.push(`Type: ${memory.memoryType}`);
        }
        if (memory.tags && memory.tags.length > 0) {
            parts.push(`Tags: ${memory.tags.join(', ')}`);
        }
        return EmbeddingService.prepareTextForEmbedding(parts.join('\n\n'));
    }
    /**
     * Create a combined text for embedding from entity data
     */
    static createEntityEmbeddingText(entity) {
        const parts = [entity.name];
        if (entity.description)
            parts.push(entity.description);
        if (entity.company)
            parts.push(`Company: ${entity.company}`);
        if (entity.title)
            parts.push(`Title: ${entity.title}`);
        if (entity.notes)
            parts.push(entity.notes);
        if (entity.tags && entity.tags.length > 0) {
            parts.push(`Tags: ${entity.tags.join(', ')}`);
        }
        return EmbeddingService.prepareTextForEmbedding(parts.join('\n\n'));
    }
    /**
     * Batch process embeddings with rate limiting
     */
    async generateEmbeddingsBatch(texts, batchSize = 100, delayMs = 1000) {
        const results = [];
        for (let i = 0; i < texts.length; i += batchSize) {
            const batch = texts.slice(i, i + batchSize);
            const embeddings = await this.generateEmbeddings(batch);
            results.push(...embeddings);
            // Add delay between batches to respect rate limits
            if (i + batchSize < texts.length) {
                await new Promise(resolve => setTimeout(resolve, delayMs));
            }
        }
        return results;
    }
    /**
     * Calculate embedding statistics
     */
    static calculateEmbeddingStats(embeddings) {
        if (embeddings.length === 0) {
            return { dimensions: 0, count: 0, avgMagnitude: 0, minMagnitude: 0, maxMagnitude: 0 };
        }
        const dimensions = embeddings[0]?.length || 0;
        const magnitudes = embeddings.map(emb => Math.sqrt(emb.reduce((sum, val) => sum + val * val, 0)));
        return {
            dimensions,
            count: embeddings.length,
            avgMagnitude: magnitudes.reduce((sum, mag) => sum + mag, 0) / magnitudes.length,
            minMagnitude: Math.min(...magnitudes),
            maxMagnitude: Math.max(...magnitudes),
        };
    }
    /**
     * Normalize vector to unit length
     */
    static normalizeVector(vector) {
        const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
        return magnitude > 0 ? vector.map(val => val / magnitude) : vector;
    }
    /**
     * Calculate dot product between two vectors
     */
    static dotProduct(a, b) {
        if (a.length !== b.length) {
            throw new Error('Vectors must have the same length');
        }
        return a.reduce((sum, val, i) => sum + val * (b[i] || 0), 0);
    }
    /**
     * Calculate Euclidean distance between two vectors
     */
    static euclideanDistance(a, b) {
        if (a.length !== b.length) {
            throw new Error('Vectors must have the same length');
        }
        const sumSquares = a.reduce((sum, val, i) => {
            const diff = val - (b[i] || 0);
            return sum + diff * diff;
        }, 0);
        return Math.sqrt(sumSquares);
    }
    /**
     * Calculate cost from token count
     */
    calculateCost(tokens) {
        return (tokens / 1_000_000) * this.EMBEDDING_PRICE_PER_1M;
    }
    /**
     * Record usage to database
     */
    async recordUsage(params) {
        if (!this.usageTracker)
            return;
        try {
            await this.usageTracker.recordUsage({
                userId: params.userId,
                apiProvider: 'openai',
                model: this.model,
                tokensUsed: params.tokensUsed,
                costUsd: params.cost,
                operationType: 'embedding',
                timestamp: Date.now(),
                date: new Date().toISOString().split('T')[0],
                metadata: {
                    duration: params.duration,
                    batchSize: params.batchSize || 1
                }
            });
        }
        catch (error) {
            console.error('[EmbeddingService] ⚠️  Failed to record usage:', error);
        }
    }
    /**
     * Estimate token count before API call
     */
    estimateTokens(text) {
        try {
            const encoder = encoding_for_model('text-embedding-3-small');
            const tokens = encoder.encode(text);
            const count = tokens.length;
            encoder.free();
            return count;
        }
        catch (error) {
            // Fallback: rough estimation (4 chars ≈ 1 token)
            return Math.ceil(text.length / 4);
        }
    }
    /**
     * Estimate cost before API call
     */
    estimateCost(text) {
        const tokens = this.estimateTokens(text);
        return this.calculateCost(tokens);
    }
    /**
     * Get daily usage summary
     */
    async getDailyUsage(userId) {
        if (!this.usageTracker) {
            return { tokens: 0, cost: 0, requests: 0 };
        }
        const today = new Date().toISOString().split('T')[0];
        const usage = await this.usageTracker.getDailyUsage(userId, today);
        return usage.openai;
    }
}
//# sourceMappingURL=embeddings.js.map