/**
 * OpenAI 임베딩 서비스
 * text-embedding-3 시리즈를 활용해 고차원 임베딩 제공
 */
import OpenAI from 'openai';
import { mementoConfig } from '../../../shared/config/index.js';
import { PIIMasker } from '../../../shared/utils/pii-masker.js';
import { logger } from '../../../shared/utils/logger.js';
import { LightweightEmbeddingService } from './lightweight-embedding-service.js';
import { RetryManager } from '../../../infrastructure/scheduler/retry-manager.js';
import { getRetryOptions } from '../../../shared/config/retry-options-loader.js';
export class OpenAIEmbeddingService {
    client = null;
    model;
    dimensions;
    maxTokens;
    cache = new Map();
    fallbackService = new LightweightEmbeddingService();
    retryManager;
    constructor() {
        this.model = mementoConfig.openaiModel || 'text-embedding-3-small';
        // text-embedding-3-small = 1536, text-embedding-3-large = 3072
        this.dimensions = mementoConfig.embeddingDimensions || 1536;
        this.maxTokens = 8191; // OpenAI 공식 문서 기준
        // RetryManager 초기화 (embedding_api 설정 사용)
        const retryOptions = getRetryOptions();
        const retryConfig = {
            ...retryOptions.embedding_api,
            maxErrorCount: retryOptions.default.maxErrorCount
        };
        this.retryManager = new RetryManager(retryConfig);
        this.initializeClient();
    }
    /**
     * OpenAI 클라이언트를 초기화
     */
    initializeClient() {
        if (!mementoConfig.openaiApiKey) {
            // 경고 로그는 MCP 프로토콜 준수를 위해 출력하지 않음
            this.client = null;
            return;
        }
        try {
            this.client = new OpenAI({ apiKey: mementoConfig.openaiApiKey });
            // 초기화 로그는 MCP 프로토콜 준수를 위해 출력하지 않음
        }
        catch (error) {
            const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
            process.stderr.write(`❌ OpenAI 초기화 실패. OPENAI_API_KEY 값과 네트워크 접근 권한을 확인하거나 MiniLM/TFiDF 모델로 폴백하세요: ${maskedError.message}\n`);
            this.client = null;
        }
    }
    isAvailable() {
        return this.client !== null;
    }
    getModelInfo() {
        return {
            model: this.model,
            dimensions: this.dimensions,
            maxTokens: this.maxTokens
        };
    }
    async generateEmbedding(text) {
        this.validateInput(text);
        const cacheKey = this.generateCacheKey(text);
        const cached = this.cache.get(cacheKey);
        if (cached) {
            return cached;
        }
        try {
            if (!this.client && mementoConfig.openaiApiKey) {
                this.initializeClient();
            }
            const result = await this.generateOpenAIEmbedding(text);
            if (result) {
                this.cache.set(cacheKey, result);
                this.cleanupCache();
            }
            return result;
        }
        catch (error) {
            const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
            process.stderr.write(`⚠️ OpenAI 임베딩 생성 실패, 경량 서비스로 fallback: ${maskedError.message}\n`);
            return this.generateFallbackEmbedding(text, cacheKey);
        }
    }
    async searchSimilar(query, embeddings, limit = 10, threshold = 0.7) {
        if (embeddings.length === 0) {
            return [];
        }
        const queryEmbedding = await this.generateEmbedding(query);
        if (!queryEmbedding) {
            return [];
        }
        return embeddings
            .map(item => ({
            id: item.id,
            content: item.content,
            similarity: this.cosineSimilarity(queryEmbedding.embedding, item.embedding),
            score: this.cosineSimilarity(queryEmbedding.embedding, item.embedding)
        }))
            .filter(item => item.similarity >= threshold)
            .sort((a, b) => b.similarity - a.similarity)
            .slice(0, limit);
    }
    async generateOpenAIEmbedding(text) {
        if (!this.client) {
            throw new Error('OpenAI 클라이언트가 초기화되지 않았습니다.');
        }
        const truncatedText = this.truncateText(text);
        // RetryManager를 사용하여 외부 API 호출 재시도
        const retryOptions = getRetryOptions();
        const response = await this.retryManager.retry(async () => {
            return await this.client.embeddings.create({
                model: this.model,
                input: truncatedText
            });
        }, {
            maxAttempts: retryOptions.embedding_api.maxAttempts,
            baseDelay: retryOptions.embedding_api.baseDelay,
            shouldRetry: (error) => {
                // 네트워크 에러나 일시적 오류만 재시도
                const message = error.message.toLowerCase();
                return message.includes('network') ||
                    message.includes('timeout') ||
                    message.includes('rate limit') ||
                    message.includes('server error') ||
                    message.includes('503') ||
                    message.includes('502') ||
                    message.includes('500');
            },
            onRetry: (error, attempt, delay) => {
                logger.warn('OpenAI 임베딩 API 호출 재시도', {
                    attempt,
                    delay,
                    error: error.message,
                    model: this.model
                });
            }
        });
        const embedding = response.data?.[0]?.embedding ?? [];
        if (embedding.length === 0) {
            throw new Error('OpenAI 임베딩 응답이 비어 있습니다.');
        }
        return {
            embedding,
            model: this.model,
            provider: 'openai',
            usage: {
                prompt_tokens: response.usage?.prompt_tokens ?? this.estimateTokens(truncatedText),
                total_tokens: response.usage?.total_tokens ?? this.estimateTokens(truncatedText)
            }
        };
    }
    async generateFallbackEmbedding(text, cacheKey) {
        try {
            const fallbackResult = await this.fallbackService.generateEmbedding(text);
            if (fallbackResult) {
                const patchedResult = {
                    ...fallbackResult,
                    provider: fallbackResult.provider ?? 'tfidf'
                };
                this.cache.set(cacheKey, patchedResult);
                this.cleanupCache();
                return patchedResult;
            }
            return null;
        }
        catch (fallbackError) {
            const errorMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
            process.stderr.write(`❌ 경량 임베딩 fallback도 실패했습니다: ${errorMsg}\n`);
            return null;
        }
    }
    validateInput(text) {
        if (!text || text.trim().length === 0) {
            throw new Error('텍스트가 비어 있습니다');
        }
    }
    truncateText(text) {
        const estimatedTokens = this.estimateTokens(text);
        if (estimatedTokens <= this.maxTokens) {
            return text;
        }
        const maxChars = this.maxTokens * 4;
        return text.substring(0, maxChars);
    }
    estimateTokens(text) {
        return Math.ceil(text.length / 4);
    }
    cosineSimilarity(vecA, vecB) {
        if (vecA.length !== vecB.length) {
            throw new Error('벡터 차원이 일치하지 않습니다');
        }
        let dot = 0;
        let normA = 0;
        let normB = 0;
        for (let i = 0; i < vecA.length; i++) {
            const a = vecA[i] ?? 0;
            const b = vecB[i] ?? 0;
            dot += a * b;
            normA += a * a;
            normB += b * b;
        }
        const denominator = Math.sqrt(normA) * Math.sqrt(normB);
        return denominator === 0 ? 0 : dot / denominator;
    }
    generateCacheKey(text) {
        return `openai:${this.hashText(text)}`;
    }
    hashText(text) {
        let hash = 0;
        for (let i = 0; i < text.length; i++) {
            const char = text.charCodeAt(i);
            hash = ((hash << 5) - hash) + char;
            hash |= 0; // 32bit 변환
        }
        return hash.toString(36);
    }
    cleanupCache() {
        const MAX_CACHE_SIZE = 1000;
        if (this.cache.size <= MAX_CACHE_SIZE) {
            return;
        }
        const entries = Array.from(this.cache.entries());
        this.cache.clear();
        entries.slice(-MAX_CACHE_SIZE / 2).forEach(([key, value]) => {
            this.cache.set(key, value);
        });
    }
}
//# sourceMappingURL=openai-embedding-service.js.map