/**
 * 임베딩 유사도 추천 Cloud Function
 *
 * documents/{docId} 문서 저장 시 Gemini embedding API로 유사 문서를 찾아
 * 정적 매칭 결과와 병합하여 ai_suggestions 서브컬렉션에 저장합니다.
 *
 * Phase 1-B: 임베딩 유사도 추천 백엔드
 * 생성일: 2026-03-23
 */

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { FieldValue: FirestoreFieldValue } = require('@google-cloud/firestore') as { FieldValue: { vector: (values: number[]) => unknown } };
import { createHash } from 'crypto';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { findMatchingTerms, type MatchResult, type LinkMethod } from './staticMatching';

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

const db = admin.firestore();

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

export interface EmbeddingMatchResult {
    targetDocId: string;
    targetTitle: string;
    method: 'embedding';
    confidence: number;   // 코사인 유사도 * 100 (정수)
    similarity: number;   // 원본 코사인 유사도 (0-1)
    explanation: string;
}

export interface MergedResult {
    targetDocId: string;
    targetTitle: string;
    method: LinkMethod;
    confidence: number;
    explanation: string;
}

interface EmbeddingMatchingConfig {
    enabled: boolean;
    minSimilarity: number;
}

interface FullAiLinkingConfig {
    enabled: boolean;
    maxSuggestions: number;
    minConfidence: number;
    autoApproveThreshold: number;
    termsCacheTTL: number;
    staticMatching: {
        enabled: boolean;
        minConfidence: number;
        maxLinksPerDoc: number;
    };
    embeddingMatching: EmbeddingMatchingConfig;
}

// ============================================================
// 순수 유틸리티 함수
// ============================================================

/**
 * 문서 content의 SHA-256 해시 계산
 */
export function computeContentHash(content: string): string {
    return createHash('sha256').update(content, 'utf8').digest('hex');
}

/**
 * content_hash와 embedding_hash 비교하여 재생성 필요 여부 판단
 * @param contentHash - 현재 문서 content의 해시
 * @param embeddingHash - 마지막 embedding 생성 시 사용된 content 해시 (없으면 undefined/null)
 */
export function shouldRegenerateEmbedding(
    contentHash: string,
    embeddingHash: string | undefined | null
): boolean {
    if (embeddingHash == null) return true;
    return contentHash !== embeddingHash;
}

// ============================================================
// 병합 함수 (순수 함수, export)
// ============================================================

/**
 * 정적 매칭 결과와 임베딩 결과를 병합
 *
 * 규칙:
 * - method 필드로 구분: "static" vs "embedding"
 * - 정적 매칭 결과 우선
 * - 중복 제거 (같은 targetDocId가 양쪽에서 나올 경우 높은 confidence 유지)
 * - maxLinksPerDoc 상한 적용
 * - confidence 내림차순 정렬
 *
 * @param staticResults - 정적 매칭 결과 (MatchResult[])
 * @param embeddingResults - 임베딩 유사도 결과 (EmbeddingMatchResult[])
 * @param maxLinksPerDoc - 최대 링크 수 상한
 */
export function mergeResults(
    staticResults: MatchResult[],
    embeddingResults: EmbeddingMatchResult[],
    maxLinksPerDoc: number
): MergedResult[] {
    // targetDocId -> MergedResult 맵 (중복 제거용)
    const resultMap = new Map<string, MergedResult>();

    // 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) {
        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) {
            // 임베딩 결과가 더 높은 confidence를 가진 경우 교체
            resultMap.set(er.targetDocId, {
                targetDocId: er.targetDocId,
                targetTitle: er.targetTitle,
                method: er.method,
                confidence: er.confidence,
                explanation: er.explanation,
            });
        }
        // 기존이 더 높으면 무시
    }

    // 3. confidence 내림차순 정렬 후 maxLinksPerDoc 상한 적용
    return Array.from(resultMap.values())
        .sort((a, b) => b.confidence - a.confidence)
        .slice(0, maxLinksPerDoc);
}

// ============================================================
// Config 로드
// ============================================================

async function loadFullConfig(): Promise<FullAiLinkingConfig> {
    const defaultConfig: FullAiLinkingConfig = {
        enabled: true,
        maxSuggestions: 5,
        minConfidence: 70,
        autoApproveThreshold: 95,
        termsCacheTTL: 3600,
        staticMatching: {
            enabled: true,
            minConfidence: 70,
            maxLinksPerDoc: 10,
        },
        embeddingMatching: {
            enabled: false,
            minSimilarity: 0.75,
        },
    };

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

// ============================================================
// Gemini Embedding API
// ============================================================

/**
 * Gemini gemini-embedding-001 모델로 텍스트 임베딩 벡터 생성 (3072차원)
 */
async function generateEmbedding(text: string): Promise<number[]> {
    const apiKey = process.env.GEMINI_API_KEY;
    if (!apiKey) {
        throw new Error('[embeddingMatching] GEMINI_API_KEY 환경변수 미설정');
    }

    const genAI = new GoogleGenerativeAI(apiKey);
    const model = genAI.getGenerativeModel({ model: 'gemini-embedding-001' });

    const result = await model.embedContent(text);
    return result.embedding.values;
}

/**
 * 임베딩 벡터를 documents/{docId}/embeddings 서브컬렉션에 저장하고
 * 문서의 embedding_hash 필드를 업데이트
 */
async function saveEmbedding(
    docId: string,
    embedding: number[],
    contentHash: string
): Promise<void> {
    const batch = db.batch();

    // embeddings 서브컬렉션에 벡터 저장
    const embeddingRef = db
        .collection('documents')
        .doc(docId)
        .collection('embeddings')
        .doc('main');

    batch.set(embeddingRef, {
        vector: FirestoreFieldValue.vector(embedding),
        model: 'gemini-embedding-001',
        dimensions: embedding.length,
        updatedAt: admin.firestore.Timestamp.now(),
        contentHash,
    });

    // 문서의 embedding_hash 업데이트
    const docRef = db.collection('documents').doc(docId);
    batch.update(docRef, {
        embedding_hash: contentHash,
        embeddingUpdatedAt: admin.firestore.Timestamp.now(),
    });

    await batch.commit();
}

// ============================================================
// Firestore Vector Search
// ============================================================

/**
 * Firestore findNearest API로 유사 문서 검색
 *
 * @param queryEmbedding - 쿼리 벡터
 * @param excludeDocId - 자기 자신 제외
 * @param minSimilarity - 최소 코사인 유사도 (0-1)
 * @param limit - 최대 결과 수
 */
async function findSimilarDocuments(
    queryEmbedding: number[],
    excludeDocId: string,
    minSimilarity: number,
    limit: number = 10
): Promise<EmbeddingMatchResult[]> {
    // distanceThreshold는 cosine distance (1 - similarity) 기준
    // cosine distance = 1 - cosine_similarity
    const distanceThreshold = 1 - minSimilarity;

    const embeddingsCollection = db.collectionGroup('embeddings');

    // @ts-ignore — firestore-vector.d.ts의 augmentation 사용
    const vectorQuery = (embeddingsCollection as unknown as import('@google-cloud/firestore').CollectionReference).findNearest({
        vectorField: 'vector',
        queryVector: queryEmbedding,
        limit,
        distanceMeasure: 'COSINE',
        distanceResultField: 'distance',
        distanceThreshold,
    });

    const snapshot = await vectorQuery.get();
    const results: EmbeddingMatchResult[] = [];

    for (const doc of snapshot.docs) {
        const data = doc.data();
        // embeddings 서브컬렉션의 부모 문서 ID 추출
        // 경로: documents/{docId}/embeddings/main
        const parentDocId = doc.ref.parent.parent?.id;
        if (!parentDocId || parentDocId === excludeDocId) continue;

        // 코사인 distance → similarity 변환
        const distance: number = data.distance ?? 0;
        const similarity = Math.max(0, Math.min(1, 1 - distance));
        const confidence = Math.round(similarity * 100);

        if (similarity < minSimilarity) continue;

        // 부모 문서 제목 조회
        const parentDocSnap = await db.collection('documents').doc(parentDocId).get();
        if (!parentDocSnap.exists) continue;

        const parentData = parentDocSnap.data();
        const targetTitle: string = parentData?.title ?? parentDocId;

        results.push({
            targetDocId: parentDocId,
            targetTitle,
            method: 'embedding',
            confidence,
            similarity,
            explanation: `embedding similarity: ${similarity.toFixed(4)} (model: gemini-embedding-001)`,
        });
    }

    return results;
}

// ============================================================
// 통합 Cloud Function 트리거
// ============================================================

/**
 * documents/{docId} onWrite 트리거
 *
 * 1. content_hash 계산 → 변경 감지
 * 2. 변경 시 embedding 재생성 (lazy)
 * 3. 정적 매칭 실행 (findMatchingTerms)
 * 4. 임베딩 유사도 검색 실행 (embeddingMatching.enabled=true인 경우만)
 * 5. 결과 병합 후 ai_suggestions 서브컬렉션에 저장
 */
export const embeddingMatching = functions.firestore
    .document('documents/{docId}')
    .onWrite(async (change, context) => {
        const docId = context.params.docId;

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

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

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

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

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

        // ── 2. content_hash 계산 및 문서 업데이트 ───────────────
        const newContentHash = computeContentHash(content);
        const existingContentHash: string | undefined = docData.content_hash;
        const existingEmbeddingHash: string | undefined = docData.embedding_hash;

        // content_hash가 변경된 경우 문서에 저장
        if (newContentHash !== existingContentHash) {
            await db.collection('documents').doc(docId).update({
                content_hash: newContentHash,
            });
            console.log(`[embeddingMatching] content_hash 업데이트: ${docId}`);
        }

        // ── 3. 정적 매칭 실행 ────────────────────────────────────
        const existingLinkIds: string[] = docData.outgoingLinkIds ?? [];
        let staticMatchResults: MatchResult[] = [];

        if (config.staticMatching.enabled) {
            try {
                const termsSnap = await db.collection('insurance_terms').get();
                const terms = termsSnap.docs.map(d => ({
                    id: d.id,
                    ...d.data(),
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                })) as any[];

                let normalizeMap: Record<string, string> = {};
                try {
                    const nmDoc = await db.collection('config').doc('normalizeMap').get();
                    if (nmDoc.exists) normalizeMap = nmDoc.data() as Record<string, string>;
                } catch { /* 무시 */ }

                staticMatchResults = findMatchingTerms(content, terms, existingLinkIds, normalizeMap)
                    .filter(r => r.confidence >= config.staticMatching.minConfidence);
            } catch (err) {
                console.warn(`[embeddingMatching] 정적 매칭 실패, 계속 진행: ${err}`);
            }
        }

        // ── 4. 임베딩 유사도 검색 ────────────────────────────────
        let embeddingMatchResults: EmbeddingMatchResult[] = [];

        if (config.embeddingMatching.enabled) {
            try {
                // embedding 재생성 필요 여부 확인
                if (shouldRegenerateEmbedding(newContentHash, existingEmbeddingHash)) {
                    console.log(`[embeddingMatching] embedding 재생성 시작: ${docId}`);
                    const embedding = await generateEmbedding(content);
                    await saveEmbedding(docId, embedding, newContentHash);
                    console.log(`[embeddingMatching] embedding 재생성 완료: ${docId}`);
                } else {
                    console.log(`[embeddingMatching] embedding 최신 상태, 재생성 스킵: ${docId}`);
                }

                // 유사 문서 검색
                const docEmbeddingSnap = await db
                    .collection('documents')
                    .doc(docId)
                    .collection('embeddings')
                    .doc('main')
                    .get();

                if (docEmbeddingSnap.exists) {
                    const embeddingData = docEmbeddingSnap.data();
                    const queryVector: number[] = embeddingData?.vector?.toArray
                        ? embeddingData.vector.toArray()
                        : (embeddingData?.vector ?? []);

                    if (queryVector.length > 0) {
                        embeddingMatchResults = await findSimilarDocuments(
                            queryVector,
                            docId,
                            config.embeddingMatching.minSimilarity,
                            config.staticMatching.maxLinksPerDoc
                        );
                    }
                }
            } catch (err) {
                console.warn(
                    `[embeddingMatching] 임베딩 처리 실패, 정적 매칭만 사용: ${err}`
                );
                // graceful degradation: 에러 시 정적 매칭만 사용
            }
        } else {
            console.log(`[embeddingMatching] embeddingMatching 비활성화, 정적 매칭만 사용: ${docId}`);
        }

        // ── 5. 결과 병합 ─────────────────────────────────────────
        const maxLinksPerDoc = config.staticMatching.maxLinksPerDoc;
        const mergedResults = mergeResults(staticMatchResults, embeddingMatchResults, maxLinksPerDoc);

        if (mergedResults.length === 0) {
            console.log(`[embeddingMatching] 병합 결과 없음: ${docId}`);
            return null;
        }

        // ── 6. ai_suggestions 저장 ────────────────────────────────
        // 이미 dismissed된 추천 조회
        const existingSuggestionsSnap = await db
            .collection('documents')
            .doc(docId)
            .collection('ai_suggestions')
            .get();

        const dismissedIds = new Set<string>();
        existingSuggestionsSnap.forEach(d => {
            if (d.data()?.dismissed === true) {
                dismissedIds.add(d.id);
            }
        });

        const batch = db.batch();
        const suggestionsRef = db
            .collection('documents')
            .doc(docId)
            .collection('ai_suggestions');

        let savedCount = 0;
        for (const result of mergedResults) {
            // dismissed된 항목 재생성 방지
            if (dismissedIds.has(result.targetDocId)) continue;

            // 자기 자신으로의 링크 제외
            if (result.targetDocId === docId) continue;

            // 기존 수동 링크 제외
            if (existingLinkIds.includes(result.targetDocId)) continue;

            const suggestionRef = suggestionsRef.doc(result.targetDocId);
            batch.set(
                suggestionRef,
                {
                    targetDocId: result.targetDocId,
                    targetTitle: result.targetTitle,
                    method: result.method,
                    confidence: result.confidence,
                    createdBy: 'system',
                    explanation: result.explanation,
                    dismissed: false,
                    createdAt: admin.firestore.Timestamp.now(),
                },
                { merge: true }
            );
            savedCount++;
        }

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

        return null;
    });
