/**
 * LLM 기반 관계 추출기
 * LLM을 사용하여 기억 간의 관계를 추출합니다.
 * 규칙 기반이 실패하거나 신뢰도가 낮은 경우 사용됩니다.
 *
 * 비용 최적화 전략:
 * - Embedding 기반 후보 제한 (cosine similarity 상위 N개)
 * - Rate limit (토큰 버킷 알고리즘, 초당 1회)
 * - 프롬프트 압축 (최대 500 토큰)
 * - 캐싱 (7일 TTL)
 * - 배치 처리 (최대 10개)
 * - 비용 모니터링
 */
import { GoogleGenerativeAI } from '@google/generative-ai';
import OpenAI from 'openai';
import { mementoConfig } from '../../../shared/config/index.js';
import { getRetryOptions } from '../../../shared/config/retry-options-loader.js';
import { CACHE, CONFIDENCE, LIMITS, LLM_COST, RATE_LIMITER } from '../../../shared/constants/relation-constants.js';
import { LLMClientInitializer } from '../../../shared/services/llm-client-initializer.js';
import { isApplicableRelationType, MEMORY_TYPE_RELATION_MAP } from '../../../shared/types/relation.js';
import { CacheKeyGenerator } from '../../../shared/utils/cache-key-generator.js';
import { logger } from '../../../shared/utils/logger.js';
import { UnifiedEmbeddingService } from '../../embedding/services/unified-embedding-service.js';
import { RelationCache } from './relation-cache.js';
import { RelationRetryManager } from './relation-retry-manager.js';
import { TokenBucketRateLimiter } from './llm-relation-extractor/token-bucket-rate-limiter.js';
import { determineRelationLlmProvider, isOllamaPreferredSlotAvailable } from './llm-relation-extractor/provider-selection.js';
import { filterRelationCandidatesByEmbedding } from './llm-relation-extractor/embedding-candidate-filter.js';
import { parseLlmRelationsResponse } from './llm-relation-extractor/llm-response-parse.js';
import { extractRelationsWithOpenAI } from './llm-relation-extractor/extract-relations-openai.js';
import { extractRelationsWithGemini } from './llm-relation-extractor/extract-relations-gemini.js';
import { extractRelationsWithOllama } from './llm-relation-extractor/extract-relations-ollama.js';
/**
 * LLM 기반 관계 추출기
 */
export class LLMBasedRelationExtractor {
    openaiClient = null;
    geminiClient = null;
    preferredProvider = null;
    initializationPromise;
    initializationCompleted = false;
    embeddingService;
    cache; // 7일 TTL
    rateLimiter;
    costMetrics;
    retryManager;
    constructor(embeddingService, cache, retryManager) {
        // 초기화 지연: preferredProvider는 null로 시작하고, initializationPromise를 통해 비동기 초기화
        this.preferredProvider = null;
        this.embeddingService = embeddingService || new UnifiedEmbeddingService();
        this.cache = cache ?? new RelationCache(CACHE.EXTRACTION_SIZE, CACHE.EXTRACTION_TTL_MS);
        this.rateLimiter = new TokenBucketRateLimiter(RATE_LIMITER.CAPACITY, RATE_LIMITER.REFILL_RATE);
        this.costMetrics = {
            totalCalls: 0,
            totalTokens: 0,
            totalCost: 0,
            lastReset: Date.now()
        };
        // RetryManager 초기화 (external_api 설정 사용)
        const retryOptions = getRetryOptions();
        this.retryManager = retryManager ?? new RelationRetryManager({
            maxAttempts: retryOptions.external_api.maxAttempts,
            baseDelay: retryOptions.external_api.baseDelay,
        });
        // 비동기 초기화: initializeClients()를 호출하고 결과를 설정
        this.initializationPromise = this.initializeClients().then((provider) => {
            this.preferredProvider = provider;
        }).catch((error) => {
            logger.error('LLM 클라이언트 초기화 실패', {
                error: error instanceof Error ? error.message : String(error)
            });
            this.preferredProvider = null;
            this.openaiClient = null;
            this.geminiClient = null;
        }).finally(() => {
            this.initializationCompleted = true;
        });
    }
    /**
     * LLM 클라이언트 초기화
     * LLMClientInitializer를 사용하여 클라이언트 초기화
     * 환경 변수 LLM_PROVIDER에 따라 프로바이더 선택
     * - 'openai': OpenAI 우선 시도, 실패 시 Gemini/Ollama fallback
     * - 'gemini': Gemini 우선 시도, 실패 시 OpenAI/Ollama fallback
     * - 'ollama': Ollama 우선 시도, 실패 시 OpenAI/Gemini fallback
     * - 'auto': 사용 가능한 것 자동 선택 (OpenAI -> Gemini -> Ollama 순서)
     */
    async initializeClients() {
        const initializer = new LLMClientInitializer();
        const result = await initializer.initialize();
        // LLMClientInitializer 결과를 사용하여 클라이언트 설정
        this.openaiClient = result.openaiClient;
        this.geminiClient = result.geminiClient;
        // 경고 메시지 로깅
        if (result.warnings.length > 0 && result.preferredProvider === null) {
            result.warnings.forEach((warning) => {
                logger.warn('LLM 초기화 경고', { warning });
            });
        }
        return result.preferredProvider;
    }
    /**
     * LLM 서비스 사용 가능 여부 확인
     */
    isAvailable() {
        if (this.preferredProvider === 'openai') {
            return this.openaiClient !== null;
        }
        if (this.preferredProvider === 'gemini') {
            return this.geminiClient !== null;
        }
        if (this.preferredProvider === 'ollama') {
            return true;
        }
        return false;
    }
    selectionContext() {
        return {
            openaiClient: this.openaiClient,
            geminiClient: this.geminiClient,
            preferredProvider: this.preferredProvider,
            llmProviderConfig: mementoConfig.llmProvider
        };
    }
    determineProvider(requestedProvider) {
        return determineRelationLlmProvider(this.selectionContext(), requestedProvider);
    }
    async filterCandidatesByEmbedding(newMemory, existingMemories, limit = LIMITS.LLM_CANDIDATE_DEFAULT) {
        return filterRelationCandidatesByEmbedding(this.embeddingService, newMemory, existingMemories, limit);
    }
    openAiDeps() {
        return {
            rateLimiter: this.rateLimiter,
            retryManager: this.retryManager,
            calculateAndLogCost: (provider, promptTokens, completionTokens) => this.calculateAndLogCost(provider, promptTokens, completionTokens),
            parseLlmRelationsResponse
        };
    }
    geminiDeps() {
        return {
            rateLimiter: this.rateLimiter,
            retryManager: this.retryManager,
            calculateAndLogCost: (provider, promptTokens, completionTokens) => this.calculateAndLogCost(provider, promptTokens, completionTokens),
            parseLlmRelationsResponse
        };
    }
    ollamaDeps() {
        return {
            rateLimiter: this.rateLimiter,
            retryManager: this.retryManager,
            calculateAndLogCost: (provider, promptTokens, completionTokens) => this.calculateAndLogCost(provider, promptTokens, completionTokens),
            parseLlmRelationsResponse
        };
    }
    async extractWithOpenAI(prompt) {
        return extractRelationsWithOpenAI(this.openaiClient, prompt, this.openAiDeps());
    }
    async extractWithGemini(prompt) {
        return extractRelationsWithGemini(this.geminiClient, prompt, this.geminiDeps());
    }
    async extractWithOllama(prompt) {
        return extractRelationsWithOllama(prompt, this.ollamaDeps());
    }
    /**
     * 관계 추출 프롬프트 템플릿 생성
     */
    buildPrompt(newMemory, existingMemories, applicableTypes) {
        const relationTypesList = applicableTypes.join(', ');
        const memoryList = existingMemories
            .map((mem, idx) => `[${idx + 1}] ID: ${mem.id}\n   내용: ${mem.content}`)
            .join('\n\n');
        return `다음은 새로운 기억과 기존 기억 목록입니다. 새로운 기억과 기존 기억들 간의 의미적 관계를 분석해주세요.

새로운 기억:
ID: ${newMemory.id}
타입: ${newMemory.type}
내용: ${newMemory.content}

기존 기억 목록:
${memoryList}

관계 유형: ${relationTypesList}

각 기존 기억에 대해 새로운 기억과의 관계가 있다면, 다음 JSON 형식으로 반환해주세요:
{
  "relations": [
    {
      "target_id": "기억_ID",
      "relation_type": "CAUSES | DEPENDS_ON | FOLLOWS | CONTRASTS_WITH | REFERENCES | BELONGS_TO",
      "confidence": 0.0~1.0,
      "reasoning": "관계 추론 근거 (선택적)"
    }
  ]
}

관계가 없는 경우 빈 배열을 반환하세요. JSON 형식만 반환하고 다른 설명은 포함하지 마세요.`;
    }
    /**
     * 기존 기억 목록을 요약하여 프롬프트 크기 축소
     * 간단한 요약: 내용을 최대 길이로 제한
     */
    compressMemories(memories, maxTokens = LIMITS.MAX_PROMPT_TOKENS) {
        const avgTokensPerMemory = 50; // 대략적인 토큰 수
        const maxMemories = Math.floor(maxTokens / avgTokensPerMemory);
        const limited = memories.slice(0, maxMemories);
        // 각 기억의 내용을 요약 (간단한 버전: 최대 200자로 제한)
        return limited.map(memory => ({
            ...memory,
            content: memory.content.length > 200
                ? memory.content.substring(0, 200) + '...'
                : memory.content
        }));
    }
    /**
     * 캐시 키 생성
     * 공통 유틸리티를 사용하여 일관된 캐시 키를 생성합니다.
     */
    generateCacheKey(newMemoryId, existingMemoryIds) {
        return CacheKeyGenerator.generateLLMRelationExtractionKey(newMemoryId, existingMemoryIds);
    }
    /**
     * 비용 계산 및 모니터링
     * 비용 계산과 모니터링 로그를 통합하여 중복을 제거합니다.
     *
     * @param provider LLM 제공자
     * @param promptTokens 프롬프트 토큰 수
     * @param completionTokens 완료 토큰 수
     * @returns 계산된 비용 (USD)
     */
    calculateAndLogCost(provider, promptTokens, completionTokens) {
        // Given: 토큰 수와 제공자 정보
        // When: 비용 계산
        let cost = 0;
        if (provider === 'openai') {
            // OpenAI 가격 (gpt-4o-mini 기준, 2025년 1월)
            const inputCost = (promptTokens / 1000) * LLM_COST.OPENAI_INPUT;
            const outputCost = (completionTokens / 1000) * LLM_COST.OPENAI_OUTPUT;
            cost = inputCost + outputCost;
        }
        else if (provider === 'gemini') {
            // Gemini 가격 (gemini-1.5-flash 기준)
            const inputCost = (promptTokens / 1000) * LLM_COST.GEMINI_INPUT;
            const outputCost = (completionTokens / 1000) * LLM_COST.GEMINI_OUTPUT;
            cost = inputCost + outputCost;
        }
        else if (provider === 'ollama') {
            // Ollama는 로컬 실행이므로 비용 0
            cost = 0;
        }
        // Then: 비용 메트릭 업데이트 및 로깅
        this.costMetrics.totalCalls++;
        this.costMetrics.totalTokens += promptTokens + completionTokens;
        this.costMetrics.totalCost += cost;
        // 주기적으로 로그 출력
        if (this.costMetrics.totalCalls % LIMITS.COST_LOG_INTERVAL === 0) {
            logger.info('LLM 비용 통계', {
                totalCalls: this.costMetrics.totalCalls,
                totalTokens: this.costMetrics.totalTokens,
                totalCost: this.costMetrics.totalCost.toFixed(4),
                provider
            });
        }
        return cost;
    }
    async extractRelations(newMemory, existingMemories, options) {
        const initializationWasPending = !this.initializationCompleted;
        if (initializationWasPending && this.preferredProvider === null) {
            throw new Error('LLM 서비스가 사용 불가능합니다');
        }
        if (this.initializationPromise && initializationWasPending) {
            await this.initializationPromise;
        }
        if (existingMemories.length === 0) {
            return [];
        }
        const hasAvailableClient = this.openaiClient !== null ||
            this.geminiClient !== null ||
            isOllamaPreferredSlotAvailable(this.selectionContext());
        if (!hasAvailableClient) {
            throw new Error('LLM 서비스를 사용할 수 없습니다. OPENAI_API_KEY 또는 GEMINI_API_KEY를 설정하거나 LLM_PROVIDER를 변경해주세요.');
        }
        // 캐시 확인
        const existingMemoryIds = existingMemories.map(m => m.id);
        const cacheKey = this.generateCacheKey(newMemory.id, existingMemoryIds);
        const cached = this.cache.get(cacheKey);
        if (cached) {
            logger.debug('LLM 관계 추출 캐시 히트', { cacheKey });
            return cached;
        }
        // 적용 가능한 관계 유형 필터링
        const allowedTypes = options?.relationTypes;
        const memoryType = newMemory.type;
        const applicableTypes = allowedTypes
            ? allowedTypes.filter(type => isApplicableRelationType(memoryType, type))
            : MEMORY_TYPE_RELATION_MAP[memoryType];
        if (applicableTypes.length === 0) {
            return [];
        }
        // Embedding 기반 후보 제한 (cosine similarity 상위 N개)
        const candidateLimit = options?.candidateLimit ?? LIMITS.LLM_CANDIDATE_DEFAULT;
        const filteredMemories = await this.filterCandidatesByEmbedding(newMemory, existingMemories, candidateLimit);
        // 프롬프트 압축
        const compressedMemories = this.compressMemories(filteredMemories, LIMITS.MAX_PROMPT_TOKENS);
        // 프롬프트 생성
        const prompt = this.buildPrompt(newMemory, compressedMemories, applicableTypes);
        // Provider 결정 (fallback 로직 포함)
        // preferredProvider가 null이거나 클라이언트가 초기화되지 않았을 때 
        // 다른 사용 가능한 provider로 자동 전환
        const requestedProvider = this.preferredProvider || mementoConfig.llmProvider || 'auto';
        const actualProvider = this.determineProvider(requestedProvider);
        // actualProvider가 null인 경우 llm_unavailable 에러 반환
        if (actualProvider === null) {
            const errorMessage = 'LLM 서비스를 사용할 수 없습니다. OPENAI_API_KEY 또는 GEMINI_API_KEY를 설정하거나 LLM_PROVIDER를 변경해주세요.';
            logger.error('LLMBasedRelationExtractor: LLM 서비스 사용 불가능', {
                requestedProvider,
                preferredProvider: this.preferredProvider,
                llmProviderConfig: mementoConfig.llmProvider,
                openaiAvailable: this.openaiClient !== null,
                geminiAvailable: this.geminiClient !== null
            });
            throw new Error(errorMessage);
        }
        // LLM 호출
        let parsedResponse;
        try {
            switch (actualProvider) {
                case 'openai':
                    parsedResponse = await this.extractWithOpenAI(prompt);
                    break;
                case 'gemini':
                    parsedResponse = await this.extractWithGemini(prompt);
                    break;
                case 'ollama':
                    parsedResponse = await this.extractWithOllama(prompt);
                    break;
                default:
                    throw new Error(`지원하지 않는 provider: ${actualProvider}`);
            }
        }
        catch (error) {
            // LLM 호출 실패 시 명확한 에러 메시지와 함께 예외를 던짐
            // 호출자가 실패를 인지하고 적절한 fallback 전략을 사용할 수 있도록 함
            const errorMessage = error instanceof Error ? error.message : String(error);
            const errorDetails = {
                error: errorMessage,
                memoryId: newMemory.id,
                provider: actualProvider,
                requestedProvider,
                preferredProvider: this.preferredProvider,
                llmProviderConfig: mementoConfig.llmProvider,
                suggestion: '규칙 기반 추출을 사용하거나 네트워크 연결을 확인하세요.'
            };
            logger.error('LLM 호출 실패', errorDetails);
            // 네트워크 오류와 실제 관계가 없는 경우를 구분하기 위해 예외를 던짐
            // 호출자는 이 예외를 catch하여 fallback 전략을 사용할 수 있음
            throw new Error(`LLM 기반 관계 추출 실패: ${errorMessage}. ${errorDetails.suggestion}`);
        }
        // 최소 신뢰도 필터링
        const minConfidence = options?.minConfidence ?? CONFIDENCE.MIN_LLM_BASED;
        // RelationCandidate로 변환
        const candidates = parsedResponse.relations
            .filter(rel => rel.confidence >= minConfidence)
            .map(rel => ({
            source_id: newMemory.id,
            target_id: rel.target_id,
            relation_type: rel.relation_type,
            confidence: rel.confidence,
            method: 'llm',
            evidence: rel.reasoning || 'LLM 분석 결과'
        }));
        // 신뢰도 내림차순 정렬
        candidates.sort((a, b) => b.confidence - a.confidence);
        // 캐시 저장 (7일 TTL)
        this.cache.set(cacheKey, candidates);
        return candidates;
    }
    /**
     * 배치 관계 추출 (여러 기억을 묶어서 처리)
     *
     * @param newMemories 새로운 기억 목록
     * @param existingMemories 기존 기억 목록
     * @param options 추출 옵션 (batchSize 포함 가능)
     * @returns 각 새로운 기억별 관계 후보 맵
     */
    async extractRelationsBatch(newMemories, existingMemories, options) {
        const results = new Map();
        // 배치 크기를 옵션에서 가져오거나, 환경 변수에서 가져오거나, 기본값 사용
        const batchSize = options?.batchSize ??
            (process.env.RELATION_EXTRACT_BATCH_SIZE ? parseInt(process.env.RELATION_EXTRACT_BATCH_SIZE, 10) : LIMITS.BATCH_SIZE_DEFAULT);
        const batches = [];
        // 배치로 나누기
        for (let i = 0; i < newMemories.length; i += batchSize) {
            batches.push(newMemories.slice(i, i + batchSize));
        }
        // 각 배치 처리
        for (const batch of batches) {
            const promises = batch.map(memory => this.extractRelations(memory, existingMemories, options));
            const batchResults = await Promise.all(promises);
            for (let i = 0; i < batch.length && i < batchResults.length; i++) {
                const memory = batch[i];
                const result = batchResults[i];
                if (memory && result !== undefined) {
                    results.set(memory.id, result);
                }
            }
        }
        return results;
    }
    /**
     * 비용 통계 조회
     */
    getCostMetrics() {
        return { ...this.costMetrics };
    }
    /**
     * 비용 통계 초기화
     */
    resetCostMetrics() {
        this.costMetrics.totalCalls = 0;
        this.costMetrics.totalTokens = 0;
        this.costMetrics.totalCost = 0;
        this.costMetrics.lastReset = Date.now();
    }
}
//# sourceMappingURL=llm-based-relation-extractor.js.map