"use strict";
/**
 * Vector Search
 *
 * Provides vector similarity search using sqlite-vss extension.
 * Falls back to brute-force cosine similarity if sqlite-vss is not available.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.VectorSearchManager = void 0;
exports.createVectorSearch = createVectorSearch;
const embedder_1 = require("./embedder");
function stripPlatformExtensionSuffix(loadablePath) {
    const ext = loadablePath.slice(loadablePath.lastIndexOf('.'));
    return ext === '.so' || ext === '.dylib' || ext === '.dll'
        ? loadablePath.slice(0, -ext.length)
        : loadablePath;
}
/**
 * Vector Search Manager
 *
 * Handles vector storage and similarity search for semantic code search.
 */
class VectorSearchManager {
    db;
    vssEnabled = false;
    embeddingDimension;
    vssLoadablePaths;
    currentModel;
    constructor(db, dimension = 768, vssLoadablePaths, currentModel) {
        this.db = db;
        this.embeddingDimension = dimension;
        this.vssLoadablePaths = vssLoadablePaths;
        this.currentModel = currentModel;
    }
    /**
     * Initialize vector search
     *
     * Attempts to load sqlite-vss extension. Falls back to brute-force
     * search if the extension is not available.
     */
    async initialize() {
        // Order matters: the `vectors` BLOB table must exist before we can scan
        // its `model` column for staleness, and any stale rows must be cleared
        // before we (re)build the VSS virtual table — otherwise the BLOB table
        // and the VSS index would disagree on row identity.
        this.ensureVectorsTable();
        // Provider→model→dimension is a pinned identity chain: a different
        // configured model means a different embedding space, regardless of
        // whether dimensions happen to match. Wipe stale corpora before either
        // mode initializes its index structures.
        if (this.currentModel) {
            this.clearIfModelChanged(this.currentModel);
        }
        if (this.vssLoadablePaths) {
            try {
                this.loadVssExtension(this.vssLoadablePaths);
                this.vssEnabled = true;
                this.createVssTable();
            }
            catch {
                // Fall back to brute-force search silently.
                this.vssEnabled = false;
            }
        }
        else {
            this.vssEnabled = false;
        }
    }
    /**
     * If `vectors` contains any rows tagged with a model other than the
     * currently configured one, drop the VSS tables (if present) and clear
     * the BLOB store. `VectorManager.embedAllNodes()` will then re-embed
     * everything on the next sync.
     */
    clearIfModelChanged(currentModel) {
        const stale = this.db
            .prepare('SELECT 1 FROM vectors WHERE model != ? LIMIT 1')
            .get(currentModel);
        if (!stale)
            return;
        // eslint-disable-next-line no-console
        console.warn(`[codegraph] stored vectors were embedded with a different model than the ` +
            `currently configured "${currentModel}". Clearing the vector store; the next ` +
            `index sync will re-embed all symbols.`);
        this.db.exec('DROP TABLE IF EXISTS vss_vectors;');
        this.db.exec('DROP TABLE IF EXISTS vss_map;');
        this.db.exec('DELETE FROM vectors;');
    }
    /**
     * Load the sqlite-vss extension
     */
    loadVssExtension(loadablePaths) {
        try {
            const sqliteDb = this.db;
            if (typeof sqliteDb.loadExtension !== 'function') {
                throw new Error('SQLite connection does not support loadExtension');
            }
            sqliteDb.loadExtension(stripPlatformExtensionSuffix(loadablePaths.vector));
            sqliteDb.loadExtension(stripPlatformExtensionSuffix(loadablePaths.vss));
        }
        catch (error) {
            throw new Error(`Failed to load sqlite-vss: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    /**
     * Create the VSS virtual table for vector search. Dimension is baked into
     * the virtual table at CREATE time; the model-change wipe in
     * `clearIfModelChanged()` guarantees we never see a stale `vss_vectors`
     * at a different dimension by the time we get here.
     */
    createVssTable() {
        const tableExists = this.db
            .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='vss_vectors'")
            .get();
        if (!tableExists) {
            // vss0 is the vector search extension.
            this.db.exec(`
        CREATE VIRTUAL TABLE IF NOT EXISTS vss_vectors USING vss0(
          embedding(${this.embeddingDimension})
        );
      `);
            this.db.exec(`
        CREATE TABLE IF NOT EXISTS vss_map (
          rowid INTEGER PRIMARY KEY,
          node_id TEXT NOT NULL UNIQUE
        );
      `);
            this.db.exec(`
        CREATE INDEX IF NOT EXISTS idx_vss_map_node ON vss_map(node_id);
      `);
        }
    }
    /**
     * Ensure the basic vectors table exists (for fallback mode)
     */
    ensureVectorsTable() {
        this.db.exec(`
      CREATE TABLE IF NOT EXISTS vectors (
        node_id TEXT PRIMARY KEY,
        embedding BLOB NOT NULL,
        model TEXT NOT NULL,
        content_hash TEXT NOT NULL DEFAULT '',
        created_at INTEGER NOT NULL
      );
    `);
        this.db.exec(`
      CREATE INDEX IF NOT EXISTS idx_vectors_model ON vectors(model);
    `);
        // Existing databases from the old semantic-search implementation only had
        // node_id, embedding, model, and created_at.
        try {
            this.db.exec(`ALTER TABLE vectors ADD COLUMN content_hash TEXT NOT NULL DEFAULT '';`);
        }
        catch {
            // Column already exists.
        }
    }
    /**
     * Check if VSS extension is enabled
     */
    isVssEnabled() {
        return this.vssEnabled;
    }
    /**
     * Store a vector embedding for a node
     *
     * @param nodeId - ID of the node
     * @param embedding - Vector embedding
     * @param model - Model used to generate embedding
     */
    storeVector(nodeId, embedding, model, contentHash = '') {
        const now = Date.now();
        // Store in the vectors table (always, for persistence)
        const blob = Buffer.from(embedding.buffer);
        this.db
            .prepare(`
        INSERT OR REPLACE INTO vectors (node_id, embedding, model, content_hash, created_at)
        VALUES (?, ?, ?, ?, ?)
      `)
            .run(nodeId, blob, model, contentHash, now);
        // Also store in VSS table if enabled
        if (this.vssEnabled) {
            this.storeInVss(nodeId, embedding);
        }
    }
    /**
     * Store vector in VSS virtual table
     */
    storeInVss(nodeId, embedding) {
        try {
            // Check if already exists
            const existing = this.db
                .prepare('SELECT rowid FROM vss_map WHERE node_id = ?')
                .get(nodeId);
            if (existing) {
                // Update existing vector
                const vectorJson = JSON.stringify(Array.from(embedding));
                this.db
                    .prepare('UPDATE vss_vectors SET embedding = ? WHERE rowid = ?')
                    .run(vectorJson, existing.rowid);
            }
            else {
                // Insert new vector - get max rowid and increment
                const maxRow = this.db
                    .prepare('SELECT MAX(rowid) as max FROM vss_map')
                    .get();
                const newRowid = (maxRow?.max ?? 0) + 1;
                const vectorJson = JSON.stringify(Array.from(embedding));
                this.db
                    .prepare('INSERT INTO vss_vectors (rowid, embedding) VALUES (?, ?)')
                    .run(newRowid, vectorJson);
                // Map the rowid to node_id
                this.db
                    .prepare('INSERT INTO vss_map (rowid, node_id) VALUES (?, ?)')
                    .run(newRowid, nodeId);
            }
        }
        catch {
            // VSS operations can fail for various reasons (dimension mismatch, etc.)
            // Fall back to brute-force search silently.
        }
    }
    /**
     * Store multiple vectors in a batch
     *
     * @param entries - Array of node IDs and embeddings
     * @param model - Model used to generate embeddings
     */
    storeVectorBatch(entries, model) {
        const now = Date.now();
        // Use a transaction for better performance
        this.db.transaction(() => {
            for (const entry of entries) {
                const blob = Buffer.from(entry.embedding.buffer);
                this.db
                    .prepare(`
            INSERT OR REPLACE INTO vectors (node_id, embedding, model, content_hash, created_at)
            VALUES (?, ?, ?, ?, ?)
          `)
                    .run(entry.nodeId, blob, model, entry.contentHash ?? '', now);
                if (this.vssEnabled) {
                    this.storeInVss(entry.nodeId, entry.embedding);
                }
            }
        })();
    }
    /**
     * Get vector for a node
     *
     * @param nodeId - ID of the node
     * @returns Embedding or null if not found
     */
    getVector(nodeId) {
        const row = this.db
            .prepare('SELECT embedding FROM vectors WHERE node_id = ?')
            .get(nodeId);
        if (!row) {
            return null;
        }
        return new Float32Array(row.embedding.buffer.slice(row.embedding.byteOffset, row.embedding.byteOffset + row.embedding.byteLength));
    }
    /**
     * Delete vector for a node
     *
     * @param nodeId - ID of the node
     */
    deleteVector(nodeId) {
        this.db.prepare('DELETE FROM vectors WHERE node_id = ?').run(nodeId);
        if (this.vssEnabled) {
            // Get the rowid before deleting
            const mapping = this.db
                .prepare('SELECT rowid FROM vss_map WHERE node_id = ?')
                .get(nodeId);
            if (mapping) {
                this.db.prepare('DELETE FROM vss_vectors WHERE rowid = ?').run(mapping.rowid);
                this.db.prepare('DELETE FROM vss_map WHERE node_id = ?').run(nodeId);
            }
        }
    }
    /**
     * Search for similar vectors
     *
     * @param queryEmbedding - Query vector to search for
     * @param options - Search options
     * @returns Array of node IDs with similarity scores
     */
    search(queryEmbedding, options = {}) {
        const { limit = 10, minScore = 0 } = options;
        if (this.vssEnabled) {
            return this.searchWithVss(queryEmbedding, limit, minScore);
        }
        else {
            return this.searchBruteForce(queryEmbedding, limit, minScore);
        }
    }
    /**
     * Search using sqlite-vss KNN search
     */
    searchWithVss(queryEmbedding, limit, minScore) {
        try {
            const vectorJson = JSON.stringify(Array.from(queryEmbedding));
            // Sanitize limit to prevent SQL injection (ensure it's a positive integer)
            const safeLimit = Math.max(1, Math.floor(limit));
            // Use VSS KNN search
            // The distance is L2 (euclidean), we need to convert to similarity score
            // Note: sqlite-vss requires LIMIT to be a literal, not a parameter
            const rows = this.db
                .prepare(`
          SELECT m.node_id, v.distance
          FROM (
            SELECT rowid, distance
            FROM vss_vectors
            WHERE vss_search(embedding, ?)
            LIMIT ${safeLimit}
          ) v
          JOIN vss_map m ON m.rowid = v.rowid
        `)
                .all(vectorJson);
            // Convert L2 distance to similarity score (1 / (1 + distance))
            return rows
                .map((row) => ({
                nodeId: row.node_id,
                score: 1 / (1 + row.distance),
            }))
                .filter((r) => r.score >= minScore);
        }
        catch {
            // VSS search failed, fall back to brute force
            return this.searchBruteForce(queryEmbedding, limit, minScore);
        }
    }
    /**
     * Brute-force search using cosine similarity
     */
    searchBruteForce(queryEmbedding, limit, minScore) {
        // Get all vectors
        const rows = this.db
            .prepare('SELECT node_id, embedding FROM vectors')
            .all();
        // Calculate cosine similarity for each
        const results = [];
        for (const row of rows) {
            const embedding = new Float32Array(row.embedding.buffer.slice(row.embedding.byteOffset, row.embedding.byteOffset + row.embedding.byteLength));
            const score = embedder_1.TextEmbedder.cosineSimilarity(queryEmbedding, embedding);
            if (score >= minScore) {
                results.push({ nodeId: row.node_id, score });
            }
        }
        // Sort by score descending and limit
        results.sort((a, b) => b.score - a.score);
        return results.slice(0, limit);
    }
    /**
     * Get count of stored vectors
     */
    getVectorCount() {
        const result = this.db
            .prepare('SELECT COUNT(*) as count FROM vectors')
            .get();
        return result.count;
    }
    /**
     * Check if a node has a vector
     */
    hasVector(nodeId) {
        const result = this.db
            .prepare('SELECT 1 FROM vectors WHERE node_id = ? LIMIT 1')
            .get(nodeId);
        return !!result;
    }
    /**
     * Check if a node has a vector matching the current embedded text.
     */
    hasCurrentVector(nodeId, model, contentHash) {
        const result = this.db
            .prepare(`
        SELECT 1 FROM vectors
        WHERE node_id = ? AND model = ? AND content_hash = ?
        LIMIT 1
      `)
            .get(nodeId, model, contentHash);
        return !!result;
    }
    /**
     * Get all node IDs that have vectors
     */
    getIndexedNodeIds() {
        const rows = this.db
            .prepare('SELECT node_id FROM vectors')
            .all();
        return rows.map((r) => r.node_id);
    }
    /**
     * Delete vectors whose node no longer exists.
     */
    deleteStaleVectors() {
        const staleIds = this.db
            .prepare(`
        SELECT v.node_id
        FROM vectors v
        LEFT JOIN nodes n ON n.id = v.node_id
        WHERE n.id IS NULL
      `)
            .all();
        for (const row of staleIds) {
            this.deleteVector(row.node_id);
        }
        return staleIds.length;
    }
    /**
     * Clear all vectors
     */
    clear() {
        this.db.prepare('DELETE FROM vectors').run();
        if (this.vssEnabled) {
            this.db.prepare('DELETE FROM vss_vectors').run();
            this.db.prepare('DELETE FROM vss_map').run();
        }
    }
    /**
     * Rebuild VSS index from vectors table
     *
     * Useful after bulk operations or if VSS index gets out of sync.
     */
    rebuildVssIndex() {
        if (!this.vssEnabled) {
            return;
        }
        // Clear VSS tables
        this.db.prepare('DELETE FROM vss_vectors').run();
        this.db.prepare('DELETE FROM vss_map').run();
        // Reload from vectors table
        const rows = this.db
            .prepare('SELECT node_id, embedding FROM vectors')
            .all();
        this.db.transaction(() => {
            for (const row of rows) {
                const embedding = new Float32Array(row.embedding.buffer.slice(row.embedding.byteOffset, row.embedding.byteOffset + row.embedding.byteLength));
                this.storeInVss(row.node_id, embedding);
            }
        })();
    }
}
exports.VectorSearchManager = VectorSearchManager;
/**
 * Create a vector search manager
 */
function createVectorSearch(db, dimension, vssLoadablePaths, currentModel) {
    return new VectorSearchManager(db, dimension, vssLoadablePaths, currentModel);
}
//# sourceMappingURL=search.js.map