/**
 * 오프라인 임베딩 시딩 + precision@5 평가 스크립트
 *
 * 동작 순서:
 *   1. Firestore에서 50개 문서 로드 (content 있는 것, createdAt desc, limit 200에서 선별)
 *   2. Gemini gemini-embedding-001 API로 각 문서의 임베딩 벡터 생성 (3072차원)
 *   3. 모든 문서 쌍에 대해 로컬 cosine similarity 계산
 *   4. similarity >= 0.75 이상인 문서를 confidence(=similarity*100) 순으로 정렬
 *   5. 각 문서의 상위 10개 결과를 Firestore ai_suggestions 서브컬렉션에 저장
 *   6. evaluate-embedding-matching.ts와 동일한 precision@5 측정 로직 실행
 *   7. 결과 JSON 파일 저장: scripts/evaluation-results-phase1b.json
 *
 * 사용법:
 *   cd /home/jay/projects/insuwiki/.worktrees/task-832.1-dev1/nextapp
 *   npx ts-node ../scripts/seed-and-evaluate-embeddings.ts
 *
 * Phase 1-B: 오프라인 임베딩 시딩 + 평가
 * 생성일: 2026-03-23
 */

import * as admin from 'firebase-admin';
import * as path from 'path';
import * as fs from 'fs';
import * as dotenv from 'dotenv';
import { GoogleGenerativeAI } from '@google/generative-ai';

// ============================================================
// 환경변수 로드 (.env.local에서 GEMINI_API_KEY 읽기)
// ============================================================
dotenv.config({ path: path.resolve(__dirname, '../nextapp/.env.local') });

const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
if (!GEMINI_API_KEY) {
    console.error('GEMINI_API_KEY가 설정되지 않았습니다. nextapp/.env.local을 확인하세요.');
    process.exit(1);
}

// ============================================================
// Firebase Admin 초기화
// ============================================================
if (!admin.apps.length) {
    const localKeyPath = path.resolve(__dirname, '../temp.j2h/insuwiki-j2h-902be7d0b6f5.json');
    if (fs.existsSync(localKeyPath)) {
        try {
            const serviceAccount = JSON.parse(fs.readFileSync(localKeyPath, 'utf8'));
            admin.initializeApp({
                credential: admin.credential.cert(serviceAccount),
                projectId: 'insuwiki-j2h',
            });
            console.log('Firebase Admin 초기화 완료 (서비스 계정 키 사용)');
        } catch {
            admin.initializeApp();
            console.warn('서비스 계정 키 파싱 실패 — 기본 인증 사용');
        }
    } else {
        admin.initializeApp();
        console.log('서비스 계정 키 없음 — 기본 인증 사용 (gcloud auth)');
    }
}

const db = admin.firestore();

// ============================================================
// Gemini AI 초기화
// ============================================================
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);

// ============================================================
// 타입 정의
// ============================================================

type LinkMethod = 'manual' | 'static' | 'embedding' | 'semantic';

interface InsuranceTerm {
    id: string;
    term: string;
    definition: string;
    commonAliases: string[];
    icdCodes?: string[];
    companyId: string;
    productId: string;
    pageNumber: number;
    verified: boolean;
    createdAt: admin.firestore.Timestamp;
}

interface MatchResult {
    termId: string;
    term: string;
    method: LinkMethod;
    confidence: number;
    explanation: string;
}

interface NormalizeMap {
    [key: string]: string;
}

interface DocumentData {
    id: string;
    title: string;
    content: string;
    outgoingLinkIds: string[];
}

interface AiSuggestion {
    targetDocId: string;
    targetTitle: string;
    method: string;
    confidence: number;
    explanation: string;
    dismissed: boolean;
    createdAt: admin.firestore.Timestamp;
}

interface MergedSuggestion {
    targetDocId: string;
    targetTitle: string;
    method: string;
    confidence: number;
    explanation: string;
}

interface EmbeddingResult {
    docId: string;
    title: string;
    vector: number[];
}

interface Phase1BEvalResult {
    evaluatedAt: string;
    totalDocuments: number;
    documentsWithEmbedding: number;
    documentsWithoutEmbedding: number;
    totalSuggestions: number;
    staticOnlyCount: number;
    embeddingOnlyCount: number;
    bothMethodCount: number;
    precisionAt5: number;
    staticPrecision: number;
    embeddingPrecision: number;  // 임베딩 매칭 precision (documents 존재 여부)
    additionalDiscoveryRate: number;
    methodDistribution: {
        static: number;
        embedding: number;
    };
    sampleResults: Array<{
        docId: string;
        title: string;
        staticCount: number;
        embeddingCount: number;
        mergedTop5: Array<{
            targetTitle: string;
            method: string;
            confidence: number;
        }>;
    }>;
}

// ============================================================
// 핵심 함수들
// ============================================================

/**
 * cosine similarity 계산 (순수 함수)
 */
function cosineSimilarity(a: number[], b: number[]): number {
    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];
    }
    const denom = Math.sqrt(normA) * Math.sqrt(normB);
    if (denom === 0) return 0;
    return dotProduct / denom;
}

/**
 * Gemini gemini-embedding-001로 임베딩 생성
 * - 텍스트가 너무 길면 앞 8000자만 사용
 * - rate limit 고려해서 호출 간 500ms 딜레이
 */
async function generateEmbedding(text: string): Promise<number[]> {
    const truncatedText = text.length > 8000 ? text.slice(0, 8000) : text;
    const embeddingModel = genAI.getGenerativeModel({ model: 'gemini-embedding-001' });
    const result = await embeddingModel.embedContent(truncatedText);
    return result.embedding.values;
}

/**
 * 지연 함수
 */
function delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// ============================================================
// findMatchingTerms 순수 함수 (functions/src/staticMatching.ts에서 복사)
// ============================================================

function findMatchingTerms(
    content: string,
    terms: InsuranceTerm[],
    existingLinkIds: string[],
    normalizeMap: NormalizeMap = {}
): MatchResult[] {
    const results: MatchResult[] = [];
    const contentLower = content.toLowerCase();
    const contentNoSpace = contentLower.replace(/\s/g, '');
    const seenTermIds = new Set<string>();
    const excludeIds = new Set(existingLinkIds);

    for (const term of terms) {
        if (excludeIds.has(term.id)) continue;
        if (seenTermIds.has(term.id)) continue;

        const termLower = term.term.toLowerCase();

        // 1. exact match → confidence: 100
        if (contentLower.includes(termLower)) {
            results.push({
                termId: term.id,
                term: term.term,
                method: 'static',
                confidence: 100,
                explanation: `exact match: "${term.term}"`,
            });
            seenTermIds.add(term.id);
            continue;
        }

        // 2. alias match (commonAliases) → confidence: 90
        let aliasMatched = false;
        for (const alias of term.commonAliases) {
            const aliasLower = alias.toLowerCase();
            if (contentLower.includes(aliasLower)) {
                results.push({
                    termId: term.id,
                    term: term.term,
                    method: 'static',
                    confidence: 90,
                    explanation: `alias match: "${alias}" → "${term.term}"`,
                });
                seenTermIds.add(term.id);
                aliasMatched = true;
                break;
            }
        }
        if (aliasMatched) continue;

        // normalizeMap 적용 후 정규화된 용어 계산
        let normalizedTermLower = termLower;
        if (normalizeMap[termLower]) {
            normalizedTermLower = normalizeMap[termLower].toLowerCase();
        }
        const normalizedNoSpace = normalizedTermLower.replace(/\s/g, '');

        // 3. 공백 제거 후 match → confidence: 85
        if (normalizedNoSpace.length > 0 && contentNoSpace.includes(normalizedNoSpace)) {
            results.push({
                termId: term.id,
                term: term.term,
                method: 'static',
                confidence: 85,
                explanation: `공백 제거 match: "${term.term}"`,
            });
            seenTermIds.add(term.id);
            continue;
        }

        // 4. substring 포함 체크 → confidence: 70 (3자 이상 용어만)
        if (termLower.length >= 3) {
            const contentWords = contentLower.split(/[\s.,!?;:()[\]{}'"]/);
            const substringMatched = contentWords.some(word => {
                if (word.length < 2) return false;
                return termLower.startsWith(word) && word.length >= 2;
            });
            if (substringMatched) {
                results.push({
                    termId: term.id,
                    term: term.term,
                    method: 'static',
                    confidence: 70,
                    explanation: `substring match: "${term.term}"`,
                });
                seenTermIds.add(term.id);
            }
        }
    }

    return results;
}

// ============================================================
// 정적 매칭 결과와 임베딩 ai_suggestions 병합
// ============================================================

function mergeStaticAndEmbedding(
    staticResults: MatchResult[],
    embeddingResults: AiSuggestion[]
): MergedSuggestion[] {
    const resultMap = new Map<string, MergedSuggestion>();

    // 1. 정적 매칭 결과 먼저 등록 (termId를 targetDocId로 사용)
    for (const sr of staticResults) {
        const existing = resultMap.get(sr.termId);
        if (!existing || sr.confidence > existing.confidence) {
            resultMap.set(sr.termId, {
                targetDocId: sr.termId,
                targetTitle: sr.term,
                method: sr.method,
                confidence: sr.confidence,
                explanation: sr.explanation,
            });
        }
    }

    // 2. 임베딩 결과 병합 (중복 시 높은 confidence 유지)
    for (const er of embeddingResults) {
        if (er.dismissed) continue;
        const existing = resultMap.get(er.targetDocId);
        if (!existing) {
            resultMap.set(er.targetDocId, {
                targetDocId: er.targetDocId,
                targetTitle: er.targetTitle,
                method: er.method,
                confidence: er.confidence,
                explanation: er.explanation,
            });
        } else if (er.confidence > existing.confidence) {
            resultMap.set(er.targetDocId, {
                targetDocId: er.targetDocId,
                targetTitle: er.targetTitle,
                method: er.method,
                confidence: er.confidence,
                explanation: er.explanation,
            });
        }
    }

    // 3. confidence 내림차순 정렬
    return Array.from(resultMap.values()).sort((a, b) => b.confidence - a.confidence);
}

// ============================================================
// Step 1: Firestore에서 문서 로드
// ============================================================

async function loadDocuments(): Promise<DocumentData[]> {
    console.log('documents 컬렉션 로드 중 (최신 50건, content 있는 것)...');
    const documents: DocumentData[] = [];

    const docsSnap = await db
        .collection('documents')
        .orderBy('createdAt', 'desc')
        .limit(200)
        .get();

    for (const doc of docsSnap.docs) {
        const data = doc.data();
        const content: string = data.content || '';
        if (!content.trim()) continue;
        documents.push({
            id: doc.id,
            title: data.title || '',
            content,
            outgoingLinkIds: data.outgoingLinkIds || [],
        });
        if (documents.length >= 50) break;
    }

    console.log(`  → ${documents.length}개 문서 로드 완료`);
    return documents;
}

// ============================================================
// Step 2: 임베딩 벡터 생성
// ============================================================

async function generateEmbeddings(documents: DocumentData[]): Promise<EmbeddingResult[]> {
    console.log(`\n임베딩 생성 시작 (총 ${documents.length}개)...`);
    const results: EmbeddingResult[] = [];

    for (let i = 0; i < documents.length; i++) {
        const doc = documents[i];
        console.log(`  [${i + 1}/${documents.length}] "${doc.title}" (${doc.id}) 임베딩 생성 중...`);
        try {
            const text = `${doc.title}\n\n${doc.content}`;
            const vector = await generateEmbedding(text);
            results.push({ docId: doc.id, title: doc.title, vector });
            console.log(`    → 완료 (차원: ${vector.length})`);
        } catch (err) {
            console.warn(`    → 실패, 스킵: ${err}`);
            // 개별 문서 실패 시 해당 문서만 스킵하고 계속 진행
        }
        // rate limit 방지: 500ms 딜레이
        if (i < documents.length - 1) {
            await delay(500);
        }
    }

    console.log(`\n임베딩 생성 완료: ${results.length}/${documents.length}개`);
    return results;
}

// ============================================================
// Step 3: 로컬 cosine similarity 계산 및 ai_suggestions 저장
// ============================================================

async function computeSimilaritiesAndSeed(
    embeddings: EmbeddingResult[],
    minSimilarity: number = 0.75,
    topK: number = 10
): Promise<Map<string, AiSuggestion[]>> {
    console.log(`\ncosine similarity 계산 및 ai_suggestions 저장 시작...`);
    console.log(`  최소 유사도: ${minSimilarity}, 상위 K: ${topK}`);

    const embeddingMap = new Map<string, EmbeddingResult>();
    for (const emb of embeddings) {
        embeddingMap.set(emb.docId, emb);
    }

    // 문서별 저장된 임베딩 결과 맵 (나중에 평가에서 재사용)
    const savedSuggestionsMap = new Map<string, AiSuggestion[]>();

    for (let i = 0; i < embeddings.length; i++) {
        const sourceEmb = embeddings[i];
        const similarities: Array<{ docId: string; title: string; similarity: number }> = [];

        // 자기 자신 제외, 모든 문서와 유사도 계산
        for (let j = 0; j < embeddings.length; j++) {
            if (i === j) continue;
            const targetEmb = embeddings[j];
            const sim = cosineSimilarity(sourceEmb.vector, targetEmb.vector);
            if (sim >= minSimilarity) {
                similarities.push({
                    docId: targetEmb.docId,
                    title: targetEmb.title,
                    similarity: sim,
                });
            }
        }

        // confidence 내림차순 정렬 후 상위 topK 선택
        similarities.sort((a, b) => b.similarity - a.similarity);
        const topSimilarities = similarities.slice(0, topK);

        console.log(
            `  [${i + 1}/${embeddings.length}] "${sourceEmb.title}": ${topSimilarities.length}개 유사 문서 발견`
        );

        if (topSimilarities.length === 0) {
            savedSuggestionsMap.set(sourceEmb.docId, []);
            continue;
        }

        // Firestore ai_suggestions 서브컬렉션에 저장
        const suggestions: AiSuggestion[] = [];
        const subColRef = db
            .collection('documents')
            .doc(sourceEmb.docId)
            .collection('ai_suggestions');

        for (const sim of topSimilarities) {
            const confidence = Math.round(sim.similarity * 100 * 100) / 100; // 소수점 2자리
            const suggestionId = `embedding_${sim.docId}`;

            // 기존 dismissed된 항목은 덮어쓰지 않음
            const existingDoc = await subColRef.doc(suggestionId).get();
            if (existingDoc.exists) {
                const existingData = existingDoc.data() as AiSuggestion;
                if (existingData.dismissed) {
                    console.log(`    → [스킵] ${sim.docId} (dismissed 상태 유지)`);
                    // dismissed 상태 그대로 수집 (평가 시 dismissed 필터링됨)
                    suggestions.push({ ...existingData });
                    continue;
                }
            }

            const suggestion: AiSuggestion = {
                targetDocId: sim.docId,
                targetTitle: sim.title,
                method: 'embedding',
                confidence,
                explanation: `cosine similarity: ${(sim.similarity * 100).toFixed(2)}%`,
                dismissed: false,
                createdAt: admin.firestore.Timestamp.now(),
            };

            await subColRef.doc(suggestionId).set(
                {
                    ...suggestion,
                    createdBy: 'system',
                },
                { merge: true }
            );
            suggestions.push(suggestion);
        }

        savedSuggestionsMap.set(sourceEmb.docId, suggestions);
    }

    console.log('\nai_suggestions 저장 완료');
    return savedSuggestionsMap;
}

// ============================================================
// Step 4: precision@5 평가
// ============================================================

async function runEvaluation(
    documents: DocumentData[],
    savedSuggestionsMap: Map<string, AiSuggestion[]>,
    terms: InsuranceTerm[],
    normalizeMap: NormalizeMap,
    validDocIdSet: Set<string>,
    evaluatedAt: string
): Promise<Phase1BEvalResult> {
    console.log('\n=== precision@5 평가 시작 ===\n');

    let documentsWithEmbedding = 0;
    let documentsWithoutEmbedding = 0;
    let totalSuggestions = 0;
    let staticOnlyCount = 0;
    let embeddingOnlyCount = 0;
    let bothMethodCount = 0;
    let methodDistributionStatic = 0;
    let methodDistributionEmbedding = 0;

    let precisionAt5Numerator = 0;
    let precisionAt5Denominator = 0;

    let staticPrecisionNumerator = 0;
    let staticPrecisionDenominator = 0;

    let embeddingPrecisionNumerator = 0;
    let embeddingPrecisionDenominator = 0;

    let additionalByEmbedding = 0;
    let staticOnlyTotal = 0;

    const termIdSet = new Set(terms.map(t => t.id));

    const sampleResults: Phase1BEvalResult['sampleResults'] = [];

    for (let i = 0; i < documents.length; i++) {
        const doc = documents[i];

        // 정적 매칭 실행
        const staticResults = findMatchingTerms(
            doc.content,
            terms,
            doc.outgoingLinkIds,
            normalizeMap
        );

        // 저장된 임베딩 결과 로드 (시딩 단계에서 저장한 것)
        let embeddingResults: AiSuggestion[] = savedSuggestionsMap.get(doc.id) || [];

        // 시딩 맵에 없는 경우 Firestore에서 직접 조회 (fallback)
        if (embeddingResults.length === 0 && !savedSuggestionsMap.has(doc.id)) {
            try {
                const suggestionsSnap = await db
                    .collection('documents')
                    .doc(doc.id)
                    .collection('ai_suggestions')
                    .where('method', '==', 'embedding')
                    .get();
                suggestionsSnap.forEach((d: admin.firestore.QueryDocumentSnapshot) => {
                    embeddingResults.push({ ...d.data() } as AiSuggestion);
                });
            } catch (err) {
                console.warn(`  [${doc.id}] ai_suggestions 조회 실패:`, err);
            }
        }

        const hasEmbedding = embeddingResults.filter(r => !r.dismissed).length > 0;
        if (hasEmbedding) {
            documentsWithEmbedding++;
        } else {
            documentsWithoutEmbedding++;
        }

        // 병합 결과 생성
        const mergedAll = mergeStaticAndEmbedding(staticResults, embeddingResults);
        const mergedTop5 = mergedAll.slice(0, 5);

        // targetDocId 유효성 검증 및 method 집계
        const staticDocIds = new Set(staticResults.map(r => r.termId));
        const embeddingDocIds = new Set(
            embeddingResults.filter(r => !r.dismissed).map(r => r.targetDocId)
        );

        for (const suggestion of mergedAll) {
            totalSuggestions++;
            const inStatic = staticDocIds.has(suggestion.targetDocId);
            const inEmbedding = embeddingDocIds.has(suggestion.targetDocId);

            if (inStatic && inEmbedding) {
                bothMethodCount++;
            } else if (inStatic) {
                staticOnlyCount++;
            } else if (inEmbedding) {
                embeddingOnlyCount++;
            }

            if (suggestion.method === 'static') {
                methodDistributionStatic++;
            } else if (suggestion.method === 'embedding') {
                methodDistributionEmbedding++;
            }
        }

        // precision@5 계산
        for (const suggestion of mergedTop5) {
            precisionAt5Denominator++;
            if (validDocIdSet.has(suggestion.targetDocId)) {
                precisionAt5Numerator++;
            }
        }

        // 정적 매칭 precision (insurance_terms 존재 여부)
        const staticTop5 = staticResults.slice(0, 5);
        for (const sr of staticTop5) {
            staticPrecisionDenominator++;
            if (termIdSet.has(sr.termId)) {
                staticPrecisionNumerator++;
            }
        }

        // 임베딩 매칭 precision (documents 존재 여부)
        const embeddingTop5 = embeddingResults.filter(r => !r.dismissed).slice(0, 5);
        for (const er of embeddingTop5) {
            embeddingPrecisionDenominator++;
            if (validDocIdSet.has(er.targetDocId)) {
                embeddingPrecisionNumerator++;
            }
        }

        // 추가 발견률 계산
        const staticIds = new Set(staticResults.map(r => r.termId));
        for (const er of embeddingResults) {
            if (er.dismissed) continue;
            if (!staticIds.has(er.targetDocId) && validDocIdSet.has(er.targetDocId)) {
                additionalByEmbedding++;
            }
        }
        staticOnlyTotal += staticResults.length;

        // sampleResults 저장
        sampleResults.push({
            docId: doc.id,
            title: doc.title,
            staticCount: staticResults.length,
            embeddingCount: embeddingResults.filter(r => !r.dismissed).length,
            mergedTop5: mergedTop5.map(r => ({
                targetTitle: r.targetTitle,
                method: r.method,
                confidence: r.confidence,
            })),
        });

        // 콘솔 출력
        console.log(`[${i + 1}] "${doc.title}" (docId: ${doc.id})`);
        console.log(
            `  정적: ${staticResults.length}건, 임베딩: ${embeddingResults.filter(r => !r.dismissed).length}건, 병합: ${mergedAll.length}건`
        );
        if (mergedTop5.length > 0) {
            console.log('  병합 Top5:');
            for (const r of mergedTop5) {
                const valid = validDocIdSet.has(r.targetDocId);
                const icon = valid ? '[O]' : '[X]';
                console.log(
                    `    ${icon} [${r.method}] "${r.targetTitle}" (confidence: ${r.confidence})`
                );
            }
        } else {
            console.log('  병합 결과 없음');
        }
    }

    // 종합 결과 계산
    const precisionAt5 = precisionAt5Denominator > 0
        ? Math.round((precisionAt5Numerator / precisionAt5Denominator) * 10000) / 100
        : 0;

    const staticPrecision = staticPrecisionDenominator > 0
        ? Math.round((staticPrecisionNumerator / staticPrecisionDenominator) * 10000) / 100
        : 0;

    const embeddingPrecision = embeddingPrecisionDenominator > 0
        ? Math.round((embeddingPrecisionNumerator / embeddingPrecisionDenominator) * 10000) / 100
        : 0;

    const totalCovered = staticOnlyTotal + additionalByEmbedding;
    const additionalDiscoveryRate = totalCovered > 0
        ? Math.round((additionalByEmbedding / totalCovered) * 10000) / 100
        : 0;

    // 종합 결과 출력
    console.log('\n\n=== 종합 결과 ===');
    console.log(`평가 문서 수: ${documents.length}`);
    console.log(`  임베딩 결과 있는 문서: ${documentsWithEmbedding}`);
    console.log(`  임베딩 결과 없는 문서: ${documentsWithoutEmbedding}`);
    console.log('');
    console.log(`총 추천 건수: ${totalSuggestions}`);
    console.log(`  정적만: ${staticOnlyCount}건`);
    console.log(`  임베딩만: ${embeddingOnlyCount}건`);
    console.log(`  양쪽 모두: ${bothMethodCount}건`);
    console.log('');
    console.log('--- Method 분포 ---');
    console.log(`  static:    ${methodDistributionStatic}건`);
    console.log(`  embedding: ${methodDistributionEmbedding}건`);
    console.log('');
    console.log('--- Precision 측정 ---');
    console.log(`  Precision (정적/insurance_terms): ${staticPrecision}% (${staticPrecisionNumerator}/${staticPrecisionDenominator})`);
    console.log(`  Precision (임베딩/documents): ${embeddingPrecision}% (${embeddingPrecisionNumerator}/${embeddingPrecisionDenominator})`);
    console.log(`  Precision@5 (병합/documents): ${precisionAt5}% (${precisionAt5Numerator}/${precisionAt5Denominator})`);
    console.log(`  추가 발견률 (임베딩): ${additionalDiscoveryRate}%`);
    console.log('');
    console.log('--- 판정 ---');
    if (precisionAt5 >= 70) {
        console.log(`[PASS] Precision@5 ${precisionAt5}% >= 70% -> Phase 1-B 목표 달성`);
    } else if (precisionAt5 >= 50) {
        console.log(`[WARN] Precision@5 ${precisionAt5}% (50-70%) -> 임베딩 설정 튜닝 필요`);
    } else {
        console.log(`[FAIL] Precision@5 ${precisionAt5}% < 50% -> minSimilarity 상향 조정 필요`);
    }

    if (additionalDiscoveryRate > 0) {
        console.log(`[INFO] 임베딩이 정적 매칭 대비 ${additionalDiscoveryRate}% 추가 발견`);
    } else {
        console.log('[INFO] 임베딩 추가 발견 없음');
    }

    return {
        evaluatedAt,
        totalDocuments: documents.length,
        documentsWithEmbedding,
        documentsWithoutEmbedding,
        totalSuggestions,
        staticOnlyCount,
        embeddingOnlyCount,
        bothMethodCount,
        precisionAt5,
        staticPrecision,
        embeddingPrecision,
        additionalDiscoveryRate,
        methodDistribution: {
            static: methodDistributionStatic,
            embedding: methodDistributionEmbedding,
        },
        sampleResults,
    };
}

// ============================================================
// 메인 함수
// ============================================================

async function main(): Promise<void> {
    const startTime = new Date();
    const evaluatedAt = startTime.toISOString();

    console.log('\n' + '='.repeat(60));
    console.log('InsuWiki Phase 1-B: 오프라인 임베딩 시딩 + 평가');
    console.log(`실행 일시: ${evaluatedAt}`);
    console.log('='.repeat(60) + '\n');

    // ── 1. insurance_terms 전체 로드 ──────────────────────────
    console.log('insurance_terms 컬렉션 로드 중...');
    let terms: InsuranceTerm[] = [];
    try {
        const termsSnap = await db.collection('insurance_terms').get();
        termsSnap.forEach((doc: admin.firestore.QueryDocumentSnapshot) => {
            terms.push({ id: doc.id, ...doc.data() } as InsuranceTerm);
        });
        console.log(`  → ${terms.length}개 용어 로드 완료`);
    } catch (err) {
        console.error('insurance_terms 로드 실패:', err);
        process.exit(1);
    }

    // ── 2. config/normalizeMap 로드 ───────────────────────────
    console.log('config/normalizeMap 로드 중...');
    let normalizeMap: NormalizeMap = {};
    try {
        const nmDoc = await db.collection('config').doc('normalizeMap').get();
        normalizeMap = nmDoc.exists ? (nmDoc.data() as NormalizeMap) : {};
        console.log(`  → ${Object.keys(normalizeMap).length}개 정규화 규칙 로드 완료`);
    } catch (err) {
        console.warn('normalizeMap 로드 실패, 빈 맵 사용:', err);
    }

    // ── 3. documents 컬렉션에서 50건 로드 ────────────────────
    let documents: DocumentData[] = [];
    try {
        documents = await loadDocuments();
    } catch (err) {
        console.error('documents 로드 실패:', err);
        process.exit(1);
    }

    if (documents.length === 0) {
        console.error('평가할 문서가 없습니다.');
        process.exit(1);
    }

    // ── 4. 실제 존재하는 document ID 목록 확인용 Set ──────────
    console.log('\ndocuments ID 목록 로드 중 (유효성 검증용)...');
    const validDocIdSet = new Set<string>();
    try {
        const allDocsSnap = await db.collection('documents').select().get();
        allDocsSnap.forEach((doc: admin.firestore.QueryDocumentSnapshot) => validDocIdSet.add(doc.id));
        console.log(`  → ${validDocIdSet.size}개 문서 ID 확인 완료`);
    } catch (err) {
        console.warn('전체 문서 ID 로드 실패, 로드된 50건만 사용:', err);
        documents.forEach(d => validDocIdSet.add(d.id));
    }

    // ── 5. 임베딩 벡터 생성 (Gemini gemini-embedding-001) ───────
    let embeddings: EmbeddingResult[] = [];
    try {
        embeddings = await generateEmbeddings(documents);
    } catch (err) {
        console.error('임베딩 생성 실패:', err);
        process.exit(1);
    }

    if (embeddings.length === 0) {
        console.error('임베딩 생성에 모두 실패했습니다.');
        process.exit(1);
    }

    // ── 6. cosine similarity 계산 및 Firestore ai_suggestions 저장 ──
    let savedSuggestionsMap: Map<string, AiSuggestion[]> = new Map();
    try {
        savedSuggestionsMap = await computeSimilaritiesAndSeed(
            embeddings,
            0.75, // minSimilarity
            10    // topK
        );
    } catch (err) {
        console.error('similarity 계산 및 시딩 실패:', err);
        process.exit(1);
    }

    // ── 7. precision@5 평가 실행 ───────────────────────────────
    let evalResult: Phase1BEvalResult;
    try {
        evalResult = await runEvaluation(
            documents,
            savedSuggestionsMap,
            terms,
            normalizeMap,
            validDocIdSet,
            evaluatedAt
        );
    } catch (err) {
        console.error('평가 실패:', err);
        process.exit(1);
    }

    // ── 8. 결과 JSON 저장 ─────────────────────────────────────
    const outputPath = path.resolve(__dirname, 'evaluation-results-phase1b.json');
    fs.writeFileSync(outputPath, JSON.stringify(evalResult, null, 2), 'utf8');
    console.log(`\n결과 파일 저장 완료: ${outputPath}`);

    const elapsed = ((Date.now() - startTime.getTime()) / 1000).toFixed(1);
    console.log(`\n총 소요 시간: ${elapsed}초`);
    console.log('\n' + '='.repeat(60) + '\n');
}

// ============================================================
// 실행
// ============================================================
main()
    .then(() => process.exit(0))
    .catch(err => {
        console.error('\n실행 실패:', err);
        process.exit(1);
    });
