/**
 * 정적 매칭 Cloud Function
 *
 * documents/{docId} 문서 저장 시 insurance_terms와 매칭하여
 * ai_suggestions 서브컬렉션에 자동 추천 결과를 저장합니다.
 *
 * 매칭 순서:
 *  1. exact match → confidence: 100
 *  2. alias match (commonAliases) → confidence: 90
 *  3. 공백 제거 후 match → confidence: 85
 *  4. substring 포함 체크 → confidence: 70
 *
 * MT-5: Cloud Function 정적 매칭 로직
 * 생성일: 2026-03-22
 */

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

// Firebase Admin 초기화 (index.ts에서 이미 초기화되어 있으나, 독립 실행 대비)
if (!admin.apps.length) {
    admin.initializeApp();
}

const db = admin.firestore();

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

export type LinkMethod = 'manual' | 'static' | 'embedding' | 'semantic';
export type LinkCreatedBy = 'user' | 'system' | 'ai';
export type LinkStatus = 'active' | 'pending';

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

export interface AiSuggestion {
    id: string;
    targetDocId: string;
    targetTitle: string;
    method: LinkMethod;
    confidence: number;      // 0-100
    createdBy: LinkCreatedBy;
    explanation?: string;
    dismissed: boolean;
    createdAt: FirebaseFirestore.Timestamp;
}

interface AiLinkingConfig {
    maxSuggestions: number;
    minConfidence: number;
    autoApproveThreshold: number;
    termsCacheTTL: number;
    enabled: boolean;
}

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

// ============================================================
// 모듈 레벨 캐시 (TTL 기반)
// ============================================================

interface TermsCache {
    terms: InsuranceTerm[];
    loadedAt: number;  // Date.now() ms
}

interface NormalizeMapCache {
    map: NormalizeMap;
    loadedAt: number;
}

let termsCache: TermsCache | null = null;
let normalizeMapCache: NormalizeMapCache | null = null;

// 기본 TTL: 1시간 (config에서 오버라이드 가능)
const DEFAULT_TERMS_CACHE_TTL_MS = 3600 * 1000;

/**
 * insurance_terms 컬렉션 로드 (메모리 캐싱, TTL 1시간)
 */
async function loadInsuranceTerms(ttlMs: number = DEFAULT_TERMS_CACHE_TTL_MS): Promise<InsuranceTerm[]> {
    const now = Date.now();
    if (termsCache && (now - termsCache.loadedAt) < ttlMs) {
        return termsCache.terms;
    }

    const snapshot = await db.collection('insurance_terms').get();
    const terms: InsuranceTerm[] = [];
    snapshot.forEach(doc => {
        terms.push({ id: doc.id, ...doc.data() } as InsuranceTerm);
    });

    termsCache = { terms, loadedAt: now };
    console.log(`[staticMatching] insurance_terms 캐시 갱신: ${terms.length}개 용어`);
    return terms;
}

/**
 * config/normalizeMap 문서 로드 (메모리 캐싱)
 */
async function loadNormalizeMap(): Promise<NormalizeMap> {
    const now = Date.now();
    // normalizeMap은 변경 빈도가 낮으므로 1시간 TTL
    if (normalizeMapCache && (now - normalizeMapCache.loadedAt) < DEFAULT_TERMS_CACHE_TTL_MS) {
        return normalizeMapCache.map;
    }

    try {
        const doc = await db.collection('config').doc('normalizeMap').get();
        const map: NormalizeMap = doc.exists ? (doc.data() as NormalizeMap) : {};
        normalizeMapCache = { map, loadedAt: now };
        return map;
    } catch (err) {
        console.warn('[staticMatching] normalizeMap 로드 실패, 빈 맵 사용:', err);
        return {};
    }
}

/**
 * config/aiLinking 문서에서 설정 로드
 */
async function loadAiLinkingConfig(): Promise<AiLinkingConfig> {
    const defaultConfig: AiLinkingConfig = {
        maxSuggestions: 5,
        minConfidence: 70,
        autoApproveThreshold: 95,
        termsCacheTTL: 3600,
        enabled: true,
    };

    try {
        const doc = await db.collection('config').doc('aiLinking').get();
        if (!doc.exists) return defaultConfig;
        return { ...defaultConfig, ...doc.data() } as AiLinkingConfig;
    } catch (err) {
        console.warn('[staticMatching] aiLinking config 로드 실패, 기본값 사용:', err);
        return defaultConfig;
    }
}

// ============================================================
// 매칭 로직 (순수 함수 - 단위 테스트 가능)
// ============================================================

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

/**
 * 문서 content에서 매칭되는 insurance_terms 검색
 *
 * @param content - 문서 내용
 * @param terms - insurance_terms 목록
 * @param existingLinkIds - 기존 수동 링크 대상 문서 ID 목록 (제외용)
 * @param normalizeMap - 정규화 테이블
 */
export 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자 이상 용어만)
        // content의 어떤 단어(공백 구분)가 term의 앞부분과 일치하는지 확인
        // 예: term='뇌졸중진단', content에 '뇌졸중'이 있으면 매칭 (용어의 부분집합)
        if (termLower.length >= 3) {
            // term 자체가 content 내의 단어로 시작하는 경우 체크
            // content를 단어로 분리하여 term이 해당 단어로 시작하는지 확인
            const contentWords = contentLower.split(/[\s.,!?;:()[\]{}'"]/);
            const substringMatched = contentWords.some(word => {
                if (word.length < 2) return false;
                // term이 content 단어를 포함하거나, content 단어가 term의 앞부분인 경우
                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;
}

// ============================================================
// Firestore 트리거
// ============================================================

/**
 * documents/{docId} onWrite 트리거
 * 문서 저장 시 insurance_terms 용어 매칭 후 ai_suggestions 저장
 */
export const staticMatching = functions.firestore
    .document('documents/{docId}')
    .onWrite(async (change, context) => {
        const docId = context.params.docId;

        // 삭제된 경우 스킵
        if (!change.after.exists) {
            console.log(`[staticMatching] 문서 삭제됨, 스킵: ${docId}`);
            return null;
        }

        const docData = change.after.data();
        if (!docData) {
            console.warn(`[staticMatching] 문서 데이터 없음: ${docId}`);
            return null;
        }

        // ── 1. config 로드 ──────────────────────────────────────
        const config = await loadAiLinkingConfig();

        if (!config.enabled) {
            console.log(`[staticMatching] AI linking 비활성화됨, 스킵: ${docId}`);
            return null;
        }

        const content: string = docData.content || '';
        if (!content.trim()) {
            console.log(`[staticMatching] 문서 내용 없음, 스킵: ${docId}`);
            return null;
        }

        // ── 2. insurance_terms 로드 (캐싱) ──────────────────────
        const ttlMs = (config.termsCacheTTL || 3600) * 1000;
        const terms = await loadInsuranceTerms(ttlMs);

        if (terms.length === 0) {
            console.log(`[staticMatching] insurance_terms 없음, 스킵: ${docId}`);
            return null;
        }

        // ── 3. normalizeMap 로드 (캐싱) ──────────────────────────
        const normalizeMap = await loadNormalizeMap();

        // ── 4. 기존 수동 링크 ID 목록 (중복 제외용) ──────────────
        const existingLinkIds: string[] = docData.outgoingLinkIds || [];

        // ── 5. 이미 dismissed된 추천 목록 로드 ──────────────────
        const existingSuggestionsSnap = await db
            .collection('documents')
            .doc(docId)
            .collection('ai_suggestions')
            .get();

        const dismissedTermIds = new Set<string>();
        const existingSuggestionIds = new Set<string>();

        existingSuggestionsSnap.forEach(doc => {
            const suggestion = doc.data() as AiSuggestion;
            existingSuggestionIds.add(doc.id);
            if (suggestion.dismissed) {
                dismissedTermIds.add(doc.id); // doc.id = termId
            }
        });

        // ── 6. 용어 매칭 ──────────────────────────────────────────
        const matchResults = findMatchingTerms(content, terms, existingLinkIds, normalizeMap);

        // ── 7. dismissed 및 minConfidence 필터링 ─────────────────
        const filtered = matchResults
            .filter(r => !dismissedTermIds.has(r.termId))
            .filter(r => r.confidence >= config.minConfidence)
            .sort((a, b) => b.confidence - a.confidence)
            .slice(0, config.maxSuggestions);

        if (filtered.length === 0) {
            console.log(`[staticMatching] 유효한 매칭 결과 없음: ${docId}`);
            return null;
        }

        // ── 8. 매칭된 용어를 title로 가진 문서 검색 ──────────────
        // Step 3: 매칭된 term의 title로 documents 컬렉션에서 대상 문서 찾기
        const suggestionPromises = filtered.map(async (matchResult) => {
            const termQuerySnap = await db
                .collection('documents')
                .where('title', '==', matchResult.term)
                .limit(1)
                .get();

            if (termQuerySnap.empty) {
                console.log(`[staticMatching] "${matchResult.term}" 제목의 문서 없음`);
                return null;
            }

            const targetDoc = termQuerySnap.docs[0];
            const targetDocId = targetDoc.id;

            // 자기 자신으로의 링크 제외
            if (targetDocId === docId) return null;

            // 이미 존재하는 수동 링크 제외 (outgoingLinkIds)
            if (existingLinkIds.includes(targetDocId)) return null;

            return {
                matchResult,
                targetDocId,
                targetTitle: matchResult.term,
            };
        });

        const resolvedSuggestions = (await Promise.all(suggestionPromises))
            .filter((s): s is NonNullable<typeof s> => s !== null);

        if (resolvedSuggestions.length === 0) {
            console.log(`[staticMatching] 대상 문서 없음: ${docId}`);
            return null;
        }

        // ── 9. ai_suggestions 서브컬렉션에 저장 ─────────────────
        const batch = db.batch();
        const suggestionsRef = db
            .collection('documents')
            .doc(docId)
            .collection('ai_suggestions');

        let savedCount = 0;
        for (const suggestion of resolvedSuggestions) {
            const suggestionDocRef = suggestionsRef.doc(suggestion.matchResult.termId);

            // 이미 dismissed된 경우 스킵 (재생성 방지)
            if (dismissedTermIds.has(suggestion.matchResult.termId)) continue;

            const suggestionData: Omit<AiSuggestion, 'id'> = {
                targetDocId: suggestion.targetDocId,
                targetTitle: suggestion.targetTitle,
                method: suggestion.matchResult.method,
                confidence: suggestion.matchResult.confidence,
                createdBy: 'system',
                explanation: suggestion.matchResult.explanation,
                dismissed: false,
                createdAt: admin.firestore.Timestamp.now(),
            };

            batch.set(suggestionDocRef, suggestionData, { merge: true });
            savedCount++;
        }

        if (savedCount > 0) {
            await batch.commit();
            console.log(
                `[staticMatching] docId=${docId}: ${savedCount}개 AI 추천 저장 완료`
            );
        }

        return null;
    });
