/**
 * 임베딩 유사도 매칭 평가 스크립트
 *
 * InsuWiki 기존 문서 50건을 대상으로 임베딩+정적 병합 결과의 precision@5를 측정하고
 * 정적 매칭 대비 추가 발견률(recall 개선)을 확인합니다.
 *
 * 사용법:
 *   npx ts-node scripts/evaluate-embedding-matching.ts
 *
 * Phase 1-B 품질 검증 스크립트
 * 생성일: 2026-03-23
 */

import * as admin from 'firebase-admin';
import * as path from 'path';
import * as fs from 'fs';

// ============================================================
// 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',
            });
        } catch {
            admin.initializeApp();
            console.warn('⚠️ 서비스 계정 키 파싱 실패 — 기본 인증 사용');
        }
    } else {
        admin.initializeApp();
        console.log('ℹ️ 서비스 계정 키 없음 — 기본 인증 사용 (gcloud auth)');
    }
}

const db = admin.firestore();

// ============================================================
// 타입 정의 (functions/src/staticMatching.ts에서 복사)
// ============================================================

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;
}

// ============================================================
// Phase 1-B 평가 결과 타입
// ============================================================

interface Phase1BEvalResult {
    evaluatedAt: string;
    totalDocuments: number;
    documentsWithEmbedding: number;
    documentsWithoutEmbedding: number;
    totalSuggestions: number;
    staticOnlyCount: number;
    embeddingOnlyCount: number;
    bothMethodCount: number;
    precisionAt5: number;           // 상위 5개 추천의 precision
    staticPrecision: number;        // 정적 매칭만의 precision
    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;
        }>;
    }>;
}

// ai_suggestions 서브컬렉션 문서 타입
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;
}

// ============================================================
// 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[] {
    // targetDocId -> 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);
}

// ============================================================
// 메인 평가 함수
// ============================================================

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

    console.log('\n=== InsuWiki Phase 1-B 임베딩 매칭 평가 ===');
    console.log(`평가 일시: ${evaluatedAt}`);
    console.log('');

    // ── 1. insurance_terms 전체 로드 ────────────────────────
    console.log('insurance_terms 컬렉션 로드 중...');
    let terms: InsuranceTerm[] = [];
    try {
        const termsSnap = await db.collection('insurance_terms').get();
        termsSnap.forEach(doc => {
            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건 로드 (content 있는 것) ──
    console.log('documents 컬렉션 로드 중 (최신 50건, content 있는 것)...');
    let documents: Array<{
        id: string;
        title: string;
        content: string;
        outgoingLinkIds: string[];
    }> = [];
    try {
        const docsSnap = await db
            .collection('documents')
            .orderBy('createdAt', 'desc')
            .limit(200)  // content 없는 문서를 걸러내기 위해 넉넉하게 조회
            .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}개 문서 로드 완료`);
    } catch (err) {
        console.error('❌ documents 로드 실패:', err);
        process.exit(1);
    }

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

    // ── 4. 실제 존재하는 document ID 목록 확인용 Set ─────────
    // targetDocId 유효성 검증을 위해 문서 ID Set 구성
    // (기존 로드된 50건 + 추가로 전체 ID 목록이 필요할 수 있으므로 별도 조회)
    console.log('\ndocuments ID 목록 로드 중 (유효성 검증용)...');
    const validDocIdSet = new Set<string>();
    try {
        // select()로 ID만 가져와 메모리 절약
        const allDocsSnap = await db.collection('documents').select().get();
        allDocsSnap.forEach(doc => validDocIdSet.add(doc.id));
        console.log(`  → ${validDocIdSet.size}개 문서 ID 확인 완료`);
    } catch (err) {
        console.warn('⚠️ 전체 문서 ID 로드 실패, 로드된 50건만 사용:', err);
        documents.forEach(d => validDocIdSet.add(d.id));
    }

    console.log(`\n평가 대상: ${documents.length}개 문서`);
    console.log('');

    // ── 5. 문서별 평가 실행 ──────────────────────────────────
    console.log('--- 문서별 평가 ---');

    // 집계 변수
    let documentsWithEmbedding = 0;
    let documentsWithoutEmbedding = 0;
    let totalSuggestions = 0;
    let staticOnlyCount = 0;
    let embeddingOnlyCount = 0;
    let bothMethodCount = 0;
    let methodDistributionStatic = 0;
    let methodDistributionEmbedding = 0;

    // precision@5 계산용
    let precisionAt5Numerator = 0;   // top5 중 유효한(targetDocId 존재) 항목 수 합산
    let precisionAt5Denominator = 0; // top5 실제 평가 대상 수 합산

    // 정적 매칭만의 precision 계산용
    let staticPrecisionNumerator = 0;
    let staticPrecisionDenominator = 0;

    // 추가 발견률 계산용: 정적만 커버하는 docId Set vs 병합 결과 docId Set
    let additionalByEmbedding = 0;  // 임베딩이 새롭게 발견한 targetDocId 수 합산
    let staticOnlyTotal = 0;        // 정적 매칭 결과 수 합산 (발견률 기준)

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

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

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

        // ── 5-b. ai_suggestions 서브컬렉션에서 embedding 결과 로드 ──
        let embeddingResults: AiSuggestion[] = [];
        try {
            const suggestionsSnap = await db
                .collection('documents')
                .doc(doc.id)
                .collection('ai_suggestions')
                .where('method', '==', 'embedding')
                .get();

            suggestionsSnap.forEach(d => {
                embeddingResults.push({ ...d.data() } as AiSuggestion);
            });
        } catch (err) {
            console.warn(`  ⚠️ [${doc.id}] ai_suggestions 로드 실패:`, err);
        }

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

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

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

        // 전체 추천 건수 및 method 분포
        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++;
            }
        }

        // ── 5-e. precision@5 계산 ────────────────────────────
        // precision@5: top5 중 targetDocId가 실제 존재하는 문서인 비율
        for (const suggestion of mergedTop5) {
            precisionAt5Denominator++;
            if (validDocIdSet.has(suggestion.targetDocId)) {
                precisionAt5Numerator++;
            }
        }

        // ── 5-f. 정적 매칭만의 precision 계산 ────────────────
        // 정적 매칭 상위 5건 중 targetDocId가 실제 존재하는 비율
        const staticTop5 = staticResults.slice(0, 5);
        for (const sr of staticTop5) {
            staticPrecisionDenominator++;
            if (validDocIdSet.has(sr.termId)) {
                staticPrecisionNumerator++;
            }
        }

        // ── 5-g. 추가 발견률 계산 ────────────────────────────
        // 임베딩이 정적 매칭에서 발견하지 못한 문서를 새롭게 발견한 수
        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;

        // ── 5-h. 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(`\n[${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 ? '✅' : '❌';
                console.log(
                    `    ${icon} [${r.method}] "${r.targetTitle}" (confidence: ${r.confidence})`
                );
            }
        } else {
            console.log('  병합 결과 없음');
        }
    }

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

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

    // 추가 발견률: 임베딩이 추가로 발견한 수 / (정적 발견 수 + 임베딩 추가 발견 수)
    const totalCovered = staticOnlyTotal + additionalByEmbedding;
    const additionalDiscoveryRate = totalCovered > 0
        ? Math.round((additionalByEmbedding / totalCovered) * 10000) / 100
        : 0;

    // ── 7. 종합 결과 출력 ────────────────────────────────────
    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@5 (병합): ${precisionAt5}% (${precisionAt5Numerator}/${precisionAt5Denominator})`);
    console.log(`  Precision@5 (정적만): ${staticPrecision}% (${staticPrecisionNumerator}/${staticPrecisionDenominator})`);
    console.log(`  추가 발견률 (임베딩): ${additionalDiscoveryRate}%`);
    console.log('');

    // ── 8. 판정 ──────────────────────────────────────────────
    console.log('--- 판정 ---');
    if (precisionAt5 >= 70) {
        console.log(`✅ Precision@5 ${precisionAt5}% ≥ 70% → Phase 1-B 목표 달성`);
    } else if (precisionAt5 >= 50) {
        console.log(`⚠️ Precision@5 ${precisionAt5}% (50-70%) → 임베딩 설정 튜닝 필요`);
    } else {
        console.log(`❌ Precision@5 ${precisionAt5}% < 50% → minSimilarity 상향 조정 필요`);
    }

    if (additionalDiscoveryRate > 0) {
        console.log(`ℹ️ 임베딩이 정적 매칭 대비 ${additionalDiscoveryRate}% 추가 발견`);
    } else {
        console.log('ℹ️ 임베딩 추가 발견 없음 (ai_suggestions 미저장 상태일 수 있음)');
    }

    // ── 9. JSON 결과 파일 저장 ───────────────────────────────
    const evalResult: Phase1BEvalResult = {
        evaluatedAt,
        totalDocuments: documents.length,
        documentsWithEmbedding,
        documentsWithoutEmbedding,
        totalSuggestions,
        staticOnlyCount,
        embeddingOnlyCount,
        bothMethodCount,
        precisionAt5,
        staticPrecision,
        additionalDiscoveryRate,
        methodDistribution: {
            static: methodDistributionStatic,
            embedding: methodDistributionEmbedding,
        },
        sampleResults,
    };

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

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