"use strict";
// src/orchestrator/rag/SqliteVectorStore.ts
// v1.13.0 — Local SQLite-backed vector store (default).
//
// Uses better-sqlite3 + sqlite-vec. Zero infra, file-based, in-process
// KNN. Stored under ~/.openlife/index/codebase.db (override via
// OPENLIFE_INDEX_DIR). Mode 0o600 on the file.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SqliteVectorStore = void 0;
exports.defaultIndexDir = defaultIndexDir;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
function defaultIndexDir() {
    const override = (process.env.OPENLIFE_INDEX_DIR || '').trim();
    if (override)
        return override;
    const home = process.env.HOME || process.env.USERPROFILE || '/tmp';
    return path.join(home, '.openlife', 'index');
}
function defaultDbPath() {
    return path.join(defaultIndexDir(), 'codebase.db');
}
function ensureDir(p) {
    if (!fs.existsSync(p))
        fs.mkdirSync(p, { recursive: true });
}
class SqliteVectorStore {
    db;
    dimSize;
    dbPath;
    constructor(opts) {
        this.dimSize = opts.dim;
        this.dbPath = opts.dbPath || defaultDbPath();
        ensureDir(path.dirname(this.dbPath));
        const BetterSqlite3 = require('better-sqlite3');
        this.db = new BetterSqlite3(this.dbPath);
        try {
            fs.chmodSync(this.dbPath, 0o600);
        }
        catch { /* best effort */ }
        const sqliteVec = require('sqlite-vec');
        sqliteVec.load(this.db);
        this.db.pragma('journal_mode = WAL');
        this.db.exec(`
      CREATE TABLE IF NOT EXISTS chunks (
        id         TEXT PRIMARY KEY,
        file       TEXT NOT NULL,
        start_line INTEGER NOT NULL,
        end_line   INTEGER NOT NULL,
        content    TEXT NOT NULL,
        mtime      INTEGER NOT NULL,
        hash       TEXT NOT NULL,
        language   TEXT
      );
      CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file);
      CREATE INDEX IF NOT EXISTS idx_chunks_lang ON chunks(language);
    `);
        this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vec USING vec0(embedding float[${this.dimSize}])`);
    }
    dim() { return this.dimSize; }
    async upsert(chunks) {
        if (chunks.length === 0)
            return;
        const upsertMeta = this.db.prepare(`
      INSERT INTO chunks (id, file, start_line, end_line, content, mtime, hash, language)
      VALUES (@id, @file, @startLine, @endLine, @content, @mtime, @hash, @language)
      ON CONFLICT(id) DO UPDATE SET
        file=excluded.file,
        start_line=excluded.start_line,
        end_line=excluded.end_line,
        content=excluded.content,
        mtime=excluded.mtime,
        hash=excluded.hash,
        language=excluded.language
    `);
        const upsertVecDelete = this.db.prepare(`DELETE FROM chunk_vec WHERE rowid = (SELECT rowid FROM chunks WHERE id = ?)`);
        const insertVec = this.db.prepare(`INSERT INTO chunk_vec(rowid, embedding) VALUES ((SELECT rowid FROM chunks WHERE id = ?), ?)`);
        const tx = this.db.transaction((items) => {
            for (const c of items) {
                upsertVecDelete.run(c.id);
                upsertMeta.run({
                    id: c.id,
                    file: c.payload.file,
                    startLine: c.payload.startLine,
                    endLine: c.payload.endLine,
                    content: c.payload.content,
                    mtime: c.payload.mtime,
                    hash: c.payload.hash,
                    language: c.payload.language ?? null,
                });
                insertVec.run(c.id, vectorToBuffer(c.vector));
            }
        });
        tx(chunks);
    }
    async search(vec, topK, filter) {
        if (vec.length !== this.dimSize) {
            throw new Error(`SqliteVectorStore.search: query dim ${vec.length} ≠ store dim ${this.dimSize}`);
        }
        let sql = `
      SELECT chunks.id    AS id,
             chunks.file  AS file,
             chunks.start_line AS startLine,
             chunks.end_line   AS endLine,
             chunks.content    AS content,
             chunks.mtime      AS mtime,
             chunks.hash       AS hash,
             chunks.language   AS language,
             vec_distance_cosine(chunk_vec.embedding, ?) AS distance
      FROM chunk_vec
      JOIN chunks ON chunks.rowid = chunk_vec.rowid
    `;
        const params = [vectorToBuffer(vec)];
        const where = [];
        if (filter?.file) {
            where.push('chunks.file = ?');
            params.push(filter.file);
        }
        if (filter?.language) {
            where.push('chunks.language = ?');
            params.push(filter.language);
        }
        if (where.length > 0)
            sql += ` WHERE ${where.join(' AND ')}`;
        sql += ` ORDER BY distance ASC LIMIT ?`;
        params.push(topK);
        const rows = this.db.prepare(sql).all(...params);
        return rows.map(r => ({
            id: r.id,
            score: 1 - r.distance,
            payload: {
                file: r.file,
                startLine: r.startLine,
                endLine: r.endLine,
                content: r.content,
                mtime: r.mtime,
                hash: r.hash,
                language: r.language || undefined,
            },
        }));
    }
    async deleteByFile(filePath) {
        const rowidsStmt = this.db.prepare(`SELECT rowid FROM chunks WHERE file = ?`);
        const rows = rowidsStmt.all(filePath);
        const delVec = this.db.prepare(`DELETE FROM chunk_vec WHERE rowid = ?`);
        const delMeta = this.db.prepare(`DELETE FROM chunks WHERE file = ?`);
        const tx = this.db.transaction(() => {
            for (const r of rows)
                delVec.run(r.rowid);
            delMeta.run(filePath);
        });
        tx();
    }
    async size() {
        const row = this.db.prepare(`SELECT COUNT(*) AS n FROM chunks`).get();
        return row.n;
    }
    async close() {
        this.db.close();
    }
    /** Lookup by id (utility for incremental indexer). */
    getMetaById(id) {
        const row = this.db.prepare(`SELECT hash, mtime FROM chunks WHERE id = ?`).get(id);
        return row || null;
    }
    /** Lookup all chunks for a file (utility for incremental indexer). */
    getFileChunks(file) {
        return this.db.prepare(`SELECT id, hash, mtime FROM chunks WHERE file = ?`).all(file);
    }
}
exports.SqliteVectorStore = SqliteVectorStore;
function vectorToBuffer(vec) {
    // sqlite-vec accepts float32 arrays as raw little-endian bytes.
    const buf = Buffer.allocUnsafe(vec.length * 4);
    for (let i = 0; i < vec.length; i++)
        buf.writeFloatLE(vec[i], i * 4);
    return buf;
}
