"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.searchKnowledge = searchKnowledge;
const embedder_1 = require("../../lib/embedder");
const bm25_1 = require("../../lib/search/bm25");
const defaults_1 = require("../../lib/defaults");
/**
 * Semantic search over the knowledge graph.
 *
 * 1. Score every node by cosine similarity to the query embedding.
 * 2. Filter seeds below `minScore`, take top `topK`.
 * 3. BFS expansion via relation edges up to `bfsDepth` hops with score decay.
 * 4. De-duplicate, re-filter, sort, cap at `maxResults`.
 */
function searchKnowledge(graph, queryEmbedding, options = {}) {
    const { topK = defaults_1.SEARCH_TOP_K, bfsDepth = defaults_1.SEARCH_BFS_DEPTH, maxResults = defaults_1.SEARCH_MAX_RESULTS, minScore = defaults_1.SEARCH_MIN_SCORE, bfsDecay = defaults_1.SEARCH_BFS_DECAY, queryText, bm25Index, searchMode = 'hybrid', rrfK = defaults_1.RRF_K } = options;
    const useVector = searchMode !== 'keyword';
    const useBm25 = searchMode !== 'vector' && !!queryText && !!bm25Index;
    // --- 1. Score all nodes (skip proxy nodes) ---
    const scored = [];
    if (useVector) {
        graph.forEachNode((id, attrs) => {
            if (attrs.proxyFor)
                return;
            if (attrs.embedding.length === 0)
                return;
            scored.push({ id, score: (0, embedder_1.cosineSimilarity)(queryEmbedding, attrs.embedding) });
        });
    }
    if (useBm25) {
        const bm25Scores = bm25Index.score(queryText);
        if (useVector && scored.length > 0) {
            // RRF fusion of vector and BM25 — include all vector results (not just positive)
            const vectorMap = new Map(scored.map(s => [s.id, s.score]));
            const fused = (0, bm25_1.rrfFuse)(vectorMap, bm25Scores, rrfK);
            scored.length = 0;
            for (const [id, score] of fused)
                scored.push({ id, score });
        }
        else {
            scored.length = 0;
            for (const [id, score] of bm25Scores)
                scored.push({ id, score });
        }
        // Normalize scores to 0–1 so minScore threshold works uniformly
        const maxScore = scored.reduce((m, s) => Math.max(m, s.score), 0);
        if (maxScore > 0) {
            for (const s of scored)
                s.score /= maxScore;
        }
    }
    if (scored.length === 0)
        return [];
    scored.sort((a, b) => b.score - a.score);
    // --- 2. Filter seeds ---
    const minS = minScore;
    const seeds = scored.filter(s => s.score >= minS).slice(0, topK);
    if (seeds.length === 0)
        return [];
    // --- 3. BFS expansion ---
    const scoreMap = new Map(seeds.map(s => [s.id, s.score]));
    function bfs(startId, seedScore) {
        const queue = [
            { id: startId, depth: 0, score: seedScore },
        ];
        const visited = new Set();
        let head = 0;
        while (head < queue.length) {
            const item = queue[head++];
            if (visited.has(item.id))
                continue;
            visited.add(item.id);
            const prev = scoreMap.get(item.id) ?? -Infinity;
            if (item.score > prev)
                scoreMap.set(item.id, item.score);
            if (item.depth >= bfsDepth)
                continue;
            if (item.score * bfsDecay < minS)
                continue;
            const nextScore = item.score * bfsDecay;
            graph.outNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
            graph.inNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
        }
    }
    for (const seed of seeds) {
        bfs(seed.id, seed.score);
    }
    // --- 4. Build results (exclude proxy nodes) ---
    return [...scoreMap.entries()]
        .filter(([id, score]) => score >= minS && !graph.getNodeAttribute(id, 'proxyFor'))
        .map(([id, score]) => {
        const attrs = graph.getNodeAttributes(id);
        return {
            id,
            title: attrs.title,
            content: attrs.content,
            tags: attrs.tags,
            score,
        };
    })
        .sort((a, b) => b.score - a.score)
        .slice(0, maxResults);
}
