/**
 * Full-text search across BRAIN memory using SQLite FTS5.
 * Uses raw SQL via nativeDb because drizzle doesn't support FTS5 virtual tables.
 *
 * Falls back to LIKE queries if FTS5 is not available (some SQLite builds lack it).
 *
 * @task T5130
 * @epic T5149
 */
import { getBrainAccessor } from '../store/brain-accessor.js';
import { typedAll } from '../store/typed-query.js';
import { searchSimilar } from './brain-similarity.js';
/** Track whether FTS5 is available in the current SQLite build. */
let _fts5Available = null;
/** Track whether FTS tables have been created and indexed this session. */
let _fts5Initialized = false;
/**
 * Check if FTS5 is available in the current SQLite build.
 */
function checkFts5Available(nativeDb) {
    if (_fts5Available !== null)
        return _fts5Available;
    try {
        // Use run() to execute DDL statements
        nativeDb.prepare('CREATE VIRTUAL TABLE IF NOT EXISTS _fts5_check USING fts5(test)').run();
        nativeDb.prepare('DROP TABLE IF EXISTS _fts5_check').run();
        _fts5Available = true;
    }
    catch {
        _fts5Available = false;
    }
    return _fts5Available;
}
/**
 * Execute a DDL statement using the native database.
 * Wraps nativeDb.prepare().run() for consistent usage.
 */
function execDDL(nativeDb, sql) {
    nativeDb.prepare(sql).run();
}
/**
 * Create FTS5 virtual tables and content-sync triggers if they don't exist.
 *
 * Uses content= to sync from main tables, so inserts to main tables
 * auto-populate FTS. UPDATE/DELETE require triggers.
 *
 * @task T5130
 */
export function ensureFts5Tables(nativeDb) {
    if (!checkFts5Available(nativeDb)) {
        return false;
    }
    // Decisions FTS
    execDDL(nativeDb, `
    CREATE VIRTUAL TABLE IF NOT EXISTS brain_decisions_fts
    USING fts5(id, decision, rationale, content=brain_decisions, content_rowid=rowid)
  `);
    // Patterns FTS
    execDDL(nativeDb, `
    CREATE VIRTUAL TABLE IF NOT EXISTS brain_patterns_fts
    USING fts5(id, pattern, context, content=brain_patterns, content_rowid=rowid)
  `);
    // Learnings FTS
    execDDL(nativeDb, `
    CREATE VIRTUAL TABLE IF NOT EXISTS brain_learnings_fts
    USING fts5(id, insight, source, content=brain_learnings, content_rowid=rowid)
  `);
    // Content-sync triggers for decisions
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_decisions_ai AFTER INSERT ON brain_decisions BEGIN
      INSERT INTO brain_decisions_fts(rowid, id, decision, rationale)
      VALUES (new.rowid, new.id, new.decision, new.rationale);
    END
  `);
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_decisions_ad AFTER DELETE ON brain_decisions BEGIN
      INSERT INTO brain_decisions_fts(brain_decisions_fts, rowid, id, decision, rationale)
      VALUES('delete', old.rowid, old.id, old.decision, old.rationale);
    END
  `);
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_decisions_au AFTER UPDATE ON brain_decisions BEGIN
      INSERT INTO brain_decisions_fts(brain_decisions_fts, rowid, id, decision, rationale)
      VALUES('delete', old.rowid, old.id, old.decision, old.rationale);
      INSERT INTO brain_decisions_fts(rowid, id, decision, rationale)
      VALUES (new.rowid, new.id, new.decision, new.rationale);
    END
  `);
    // Content-sync triggers for patterns
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_patterns_ai AFTER INSERT ON brain_patterns BEGIN
      INSERT INTO brain_patterns_fts(rowid, id, pattern, context)
      VALUES (new.rowid, new.id, new.pattern, new.context);
    END
  `);
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_patterns_ad AFTER DELETE ON brain_patterns BEGIN
      INSERT INTO brain_patterns_fts(brain_patterns_fts, rowid, id, pattern, context)
      VALUES('delete', old.rowid, old.id, old.pattern, old.context);
    END
  `);
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_patterns_au AFTER UPDATE ON brain_patterns BEGIN
      INSERT INTO brain_patterns_fts(brain_patterns_fts, rowid, id, pattern, context)
      VALUES('delete', old.rowid, old.id, old.pattern, old.context);
      INSERT INTO brain_patterns_fts(rowid, id, pattern, context)
      VALUES (new.rowid, new.id, new.pattern, new.context);
    END
  `);
    // Content-sync triggers for learnings
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_learnings_ai AFTER INSERT ON brain_learnings BEGIN
      INSERT INTO brain_learnings_fts(rowid, id, insight, source)
      VALUES (new.rowid, new.id, new.insight, new.source);
    END
  `);
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_learnings_ad AFTER DELETE ON brain_learnings BEGIN
      INSERT INTO brain_learnings_fts(brain_learnings_fts, rowid, id, insight, source)
      VALUES('delete', old.rowid, old.id, old.insight, old.source);
    END
  `);
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_learnings_au AFTER UPDATE ON brain_learnings BEGIN
      INSERT INTO brain_learnings_fts(brain_learnings_fts, rowid, id, insight, source)
      VALUES('delete', old.rowid, old.id, old.insight, old.source);
      INSERT INTO brain_learnings_fts(rowid, id, insight, source)
      VALUES (new.rowid, new.id, new.insight, new.source);
    END
  `);
    // Observations FTS
    execDDL(nativeDb, `
    CREATE VIRTUAL TABLE IF NOT EXISTS brain_observations_fts
    USING fts5(id, title, narrative, content=brain_observations, content_rowid=rowid)
  `);
    // Content-sync triggers for observations
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_observations_ai AFTER INSERT ON brain_observations BEGIN
      INSERT INTO brain_observations_fts(rowid, id, title, narrative)
      VALUES (new.rowid, new.id, new.title, new.narrative);
    END
  `);
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_observations_ad AFTER DELETE ON brain_observations BEGIN
      INSERT INTO brain_observations_fts(brain_observations_fts, rowid, id, title, narrative)
      VALUES('delete', old.rowid, old.id, old.title, old.narrative);
    END
  `);
    execDDL(nativeDb, `
    CREATE TRIGGER IF NOT EXISTS brain_observations_au AFTER UPDATE ON brain_observations BEGIN
      INSERT INTO brain_observations_fts(brain_observations_fts, rowid, id, title, narrative)
      VALUES('delete', old.rowid, old.id, old.title, old.narrative);
      INSERT INTO brain_observations_fts(rowid, id, title, narrative)
      VALUES (new.rowid, new.id, new.title, new.narrative);
    END
  `);
    return true;
}
/**
 * Rebuild FTS5 indexes from the content tables.
 * Useful after bulk inserts that bypass triggers.
 *
 * @task T5130
 */
export function rebuildFts5Index(nativeDb) {
    if (!checkFts5Available(nativeDb)) {
        return;
    }
    nativeDb.prepare("INSERT INTO brain_decisions_fts(brain_decisions_fts) VALUES('rebuild')").run();
    nativeDb.prepare("INSERT INTO brain_patterns_fts(brain_patterns_fts) VALUES('rebuild')").run();
    nativeDb.prepare("INSERT INTO brain_learnings_fts(brain_learnings_fts) VALUES('rebuild')").run();
    // Observations FTS rebuild — table may not exist yet in older DBs
    try {
        nativeDb
            .prepare("INSERT INTO brain_observations_fts(brain_observations_fts) VALUES('rebuild')")
            .run();
    }
    catch {
        // brain_observations_fts not created yet — skip
    }
}
/**
 * Unified search across all BRAIN memory tables.
 *
 * Uses FTS5 MATCH for full-text search with BM25 ranking when available,
 * falls back to LIKE queries otherwise.
 *
 * @task T5130
 */
export async function searchBrain(projectRoot, query, options) {
    if (!query || !query.trim()) {
        return { decisions: [], patterns: [], learnings: [], observations: [] };
    }
    // Ensure brain.db is initialized
    const { getBrainDb, getBrainNativeDb } = await import('../store/brain-sqlite.js');
    await getBrainDb(projectRoot);
    const nativeDb = getBrainNativeDb();
    if (!nativeDb) {
        return { decisions: [], patterns: [], learnings: [], observations: [] };
    }
    const limit = options?.limit ?? 10;
    const tables = options?.tables ?? ['decisions', 'patterns', 'learnings', 'observations'];
    const ftsAvailable = ensureFts5Tables(nativeDb);
    if (ftsAvailable) {
        // On first initialization, rebuild FTS indexes to sync any data
        // that was inserted before the FTS triggers existed.
        if (!_fts5Initialized) {
            _fts5Initialized = true;
            rebuildFts5Index(nativeDb);
        }
        return searchWithFts5(nativeDb, query, tables, limit);
    }
    return searchWithLike(nativeDb, query, tables, limit);
}
/**
 * Search using FTS5 MATCH with BM25 ranking.
 */
function searchWithFts5(nativeDb, query, tables, limit) {
    const result = {
        decisions: [],
        patterns: [],
        learnings: [],
        observations: [],
    };
    // Escape FTS5 special characters for safety
    const safeQuery = escapeFts5Query(query);
    if (tables.includes('decisions')) {
        try {
            const rows = typedAll(nativeDb.prepare(`
        SELECT d.*
        FROM brain_decisions_fts fts
        JOIN brain_decisions d ON d.rowid = fts.rowid
        WHERE brain_decisions_fts MATCH ?
        ORDER BY bm25(brain_decisions_fts)
        LIMIT ?
      `), safeQuery, limit);
            result.decisions = rows;
        }
        catch {
            // FTS query failed, fall back to LIKE for this table
            result.decisions = likeSearchDecisions(nativeDb, query, limit);
        }
    }
    if (tables.includes('patterns')) {
        try {
            const rows = typedAll(nativeDb.prepare(`
        SELECT p.*
        FROM brain_patterns_fts fts
        JOIN brain_patterns p ON p.rowid = fts.rowid
        WHERE brain_patterns_fts MATCH ?
        ORDER BY bm25(brain_patterns_fts)
        LIMIT ?
      `), safeQuery, limit);
            result.patterns = rows;
        }
        catch {
            result.patterns = likeSearchPatterns(nativeDb, query, limit);
        }
    }
    if (tables.includes('learnings')) {
        try {
            const rows = typedAll(nativeDb.prepare(`
        SELECT l.*
        FROM brain_learnings_fts fts
        JOIN brain_learnings l ON l.rowid = fts.rowid
        WHERE brain_learnings_fts MATCH ?
        ORDER BY bm25(brain_learnings_fts)
        LIMIT ?
      `), safeQuery, limit);
            result.learnings = rows;
        }
        catch {
            result.learnings = likeSearchLearnings(nativeDb, query, limit);
        }
    }
    if (tables.includes('observations')) {
        try {
            const rows = typedAll(nativeDb.prepare(`
        SELECT o.*
        FROM brain_observations_fts fts
        JOIN brain_observations o ON o.rowid = fts.rowid
        WHERE brain_observations_fts MATCH ?
        ORDER BY bm25(brain_observations_fts)
        LIMIT ?
      `), safeQuery, limit);
            result.observations = rows;
        }
        catch {
            result.observations = likeSearchObservations(nativeDb, query, limit);
        }
    }
    return result;
}
/**
 * Search using LIKE queries as fallback when FTS5 is unavailable.
 */
function searchWithLike(nativeDb, query, tables, limit) {
    const result = {
        decisions: [],
        patterns: [],
        learnings: [],
        observations: [],
    };
    if (tables.includes('decisions')) {
        result.decisions = likeSearchDecisions(nativeDb, query, limit);
    }
    if (tables.includes('patterns')) {
        result.patterns = likeSearchPatterns(nativeDb, query, limit);
    }
    if (tables.includes('learnings')) {
        result.learnings = likeSearchLearnings(nativeDb, query, limit);
    }
    if (tables.includes('observations')) {
        result.observations = likeSearchObservations(nativeDb, query, limit);
    }
    return result;
}
function likeSearchDecisions(nativeDb, query, limit) {
    const likePattern = `%${query}%`;
    return typedAll(nativeDb.prepare(`
    SELECT * FROM brain_decisions
    WHERE decision LIKE ? OR rationale LIKE ?
    ORDER BY created_at DESC
    LIMIT ?
  `), likePattern, likePattern, limit);
}
function likeSearchPatterns(nativeDb, query, limit) {
    const likePattern = `%${query}%`;
    return typedAll(nativeDb.prepare(`
    SELECT * FROM brain_patterns
    WHERE pattern LIKE ? OR context LIKE ?
    ORDER BY frequency DESC
    LIMIT ?
  `), likePattern, likePattern, limit);
}
function likeSearchLearnings(nativeDb, query, limit) {
    const likePattern = `%${query}%`;
    return typedAll(nativeDb.prepare(`
    SELECT * FROM brain_learnings
    WHERE insight LIKE ? OR source LIKE ?
    ORDER BY confidence DESC
    LIMIT ?
  `), likePattern, likePattern, limit);
}
function likeSearchObservations(nativeDb, query, limit) {
    const likePattern = `%${query}%`;
    return typedAll(nativeDb.prepare(`
    SELECT * FROM brain_observations
    WHERE title LIKE ? OR narrative LIKE ?
    ORDER BY created_at DESC
    LIMIT ?
  `), likePattern, likePattern, limit);
}
/**
 * Escape special FTS5 characters in query string.
 * Wraps each token in quotes to prevent syntax errors from special chars.
 */
function escapeFts5Query(query) {
    // Split on whitespace, wrap each token in double quotes
    const tokens = query.trim().split(/\s+/).filter(Boolean);
    if (tokens.length === 0)
        return '""';
    return tokens.map((t) => `"${t.replace(/"/g, '""')}"`).join(' ');
}
/**
 * Reset the cached FTS5 availability flag.
 * Used in tests to force re-detection.
 */
export function resetFts5Cache() {
    _fts5Available = null;
    _fts5Initialized = false;
}
/**
 * Hybrid search across FTS5, vector similarity, and graph neighbors.
 *
 * 1. Runs FTS5 search via existing searchBrain.
 * 2. Runs vector similarity via searchSimilar (if available).
 * 3. Runs graph neighbor expansion via getNeighbors (if query matches a node).
 * 4. Normalizes scores to 0-1 using min-max normalization.
 * 5. Combines with configurable weights.
 * 6. Deduplicates by ID, keeping highest combined score.
 * 7. Returns top-N sorted by score descending.
 *
 * Graceful fallback: if vec unavailable, redistributes weight to FTS5.
 *
 * @param query - Search query text
 * @param projectRoot - Project root directory
 * @param options - Weight and limit configuration
 * @returns Array of hybrid results ranked by combined score
 */
export async function hybridSearch(query, projectRoot, options) {
    if (!query || !query.trim())
        return [];
    const maxResults = options?.limit ?? 10;
    let ftsWeight = options?.ftsWeight ?? 0.5;
    let vecWeight = options?.vecWeight ?? 0.4;
    const graphWeight = options?.graphWeight ?? 0.1;
    // Score accumulators: id -> { score, sources, type, title, text }
    const scoreMap = new Map();
    const addScore = (id, normalizedScore, weight, source, type, title, text) => {
        const existing = scoreMap.get(id);
        if (existing) {
            existing.score += normalizedScore * weight;
            existing.sources.add(source);
        }
        else {
            scoreMap.set(id, {
                score: normalizedScore * weight,
                type,
                title,
                text,
                sources: new Set([source]),
            });
        }
    };
    // --- 1. FTS5 search ---
    const ftsResults = await searchBrain(projectRoot, query, { limit: maxResults * 2 });
    // Collect all FTS hits into a flat list with position-based scores
    const ftsHits = [];
    for (const d of ftsResults.decisions) {
        ftsHits.push({
            id: d.id,
            type: 'decision',
            title: d.decision,
            text: `${d.decision} — ${d.rationale}`,
        });
    }
    for (const p of ftsResults.patterns) {
        ftsHits.push({
            id: p.id,
            type: 'pattern',
            title: p.pattern,
            text: `${p.pattern} — ${p.context}`,
        });
    }
    for (const l of ftsResults.learnings) {
        ftsHits.push({
            id: l.id,
            type: 'learning',
            title: l.insight,
            text: `${l.insight} (source: ${l.source})`,
        });
    }
    for (const o of ftsResults.observations) {
        ftsHits.push({ id: o.id, type: 'observation', title: o.title, text: o.narrative ?? o.title });
    }
    // Normalize FTS: position-based (first result = 1.0, last = near 0)
    for (let i = 0; i < ftsHits.length; i++) {
        const hit = ftsHits[i];
        const normalizedScore = ftsHits.length > 1 ? 1.0 - i / (ftsHits.length - 1) : 1.0;
        addScore(hit.id, normalizedScore, ftsWeight, 'fts', hit.type, hit.title, hit.text);
    }
    // --- 2. Vector similarity search ---
    let vecResults = [];
    try {
        vecResults = await searchSimilar(query, projectRoot, maxResults * 2);
    }
    catch {
        // Vector search unavailable
    }
    if (vecResults.length > 0) {
        // Normalize vector: distance-based (smaller distance = higher score)
        const maxDist = Math.max(...vecResults.map((r) => r.distance), 0.001);
        for (const r of vecResults) {
            const normalizedScore = 1.0 - r.distance / maxDist;
            addScore(r.id, normalizedScore, vecWeight, 'vec', r.type, r.title, r.text);
        }
    }
    else {
        // Redistribute vec weight to FTS if vector unavailable
        ftsWeight += vecWeight;
        vecWeight = 0;
        // Re-score FTS hits with updated weight
        scoreMap.clear();
        for (let i = 0; i < ftsHits.length; i++) {
            const hit = ftsHits[i];
            const normalizedScore = ftsHits.length > 1 ? 1.0 - i / (ftsHits.length - 1) : 1.0;
            addScore(hit.id, normalizedScore, ftsWeight, 'fts', hit.type, hit.title, hit.text);
        }
    }
    // --- 3. Graph neighbor expansion ---
    try {
        const accessor = await getBrainAccessor(projectRoot);
        // Check if query matches a known graph node ID pattern
        const possibleNodeIds = [
            `concept:${query.toLowerCase().replace(/\s+/g, '-')}`,
            `task:${query}`,
            `doc:${query}`,
        ];
        for (const nodeId of possibleNodeIds) {
            const node = await accessor.getPageNode(nodeId);
            if (!node)
                continue;
            const neighbors = await accessor.getNeighbors(nodeId);
            for (let i = 0; i < neighbors.length; i++) {
                const neighbor = neighbors[i];
                const normalizedScore = neighbors.length > 1 ? 1.0 - i / (neighbors.length - 1) : 1.0;
                addScore(neighbor.id, normalizedScore, graphWeight, 'graph', neighbor.nodeType, neighbor.label, neighbor.label);
            }
        }
    }
    catch {
        // Graph search unavailable — no redistribution needed (small weight)
    }
    // --- 4. Sort and return top-N ---
    const sorted = [...scoreMap.entries()]
        .map(([id, data]) => ({
        id,
        score: data.score,
        type: data.type,
        title: data.title,
        text: data.text,
        sources: [...data.sources],
    }))
        .sort((a, b) => b.score - a.score)
        .slice(0, maxResults);
    return sorted;
}
//# sourceMappingURL=brain-search.js.map