import { createLogger } from "../logger.js";
const log = createLogger("cache");
class SqliteVecStore {
  constructor(db) {
    this.db = db;
    this.upsertSelectStmt = db.prepare(
      "SELECT rowid FROM vec_id_map WHERE external_id = ?"
    );
    this.upsertInsertIdMapStmt = db.prepare(
      "INSERT INTO vec_id_map (external_id) VALUES (?)"
    );
    this.upsertDeleteDocStmt = db.prepare(
      "DELETE FROM vec_documents WHERE rowid = ?"
    );
    this.upsertInsertDocStmt = db.prepare(
      "INSERT INTO vec_documents (rowid, embedding) VALUES (?, ?)"
    );
    this.upsertUpsertMetadataStmt = db.prepare(`
      INSERT INTO vec_metadata (rowid, url, content_hash, model_id, created_at, extra_json)
      VALUES (?, ?, ?, ?, ?, ?)
      ON CONFLICT(rowid) DO UPDATE SET
        url = excluded.url,
        content_hash = excluded.content_hash,
        model_id = excluded.model_id,
        created_at = excluded.created_at,
        extra_json = excluded.extra_json
    `);
    this.deleteIdMapStmt = db.prepare("DELETE FROM vec_id_map WHERE external_id = ?");
    this.deleteDocStmt = db.prepare("DELETE FROM vec_documents WHERE rowid = ?");
    this.sizeStmt = db.prepare("SELECT COUNT(*) AS c FROM vec_id_map");
  }
  db;
  upsertSelectStmt;
  upsertInsertIdMapStmt;
  upsertDeleteDocStmt;
  upsertInsertDocStmt;
  upsertUpsertMetadataStmt;
  deleteIdMapStmt;
  deleteDocStmt;
  sizeStmt;
  async upsert(records) {
    if (records.length === 0) return;
    const tx = this.db.transaction((items) => {
      for (const record of items) {
        const existing = this.upsertSelectStmt.get(record.id);
        let rowid;
        if (existing) {
          rowid = existing.rowid;
          this.upsertDeleteDocStmt.run(BigInt(rowid));
        } else {
          const info = this.upsertInsertIdMapStmt.run(record.id);
          rowid = Number(info.lastInsertRowid);
        }
        const vec = record.vector;
        const buf = Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
        this.upsertInsertDocStmt.run(BigInt(rowid), buf);
        const extra = record.metadata.extra ? JSON.stringify(record.metadata.extra) : null;
        this.upsertUpsertMetadataStmt.run(
          rowid,
          record.metadata.url,
          record.metadata.contentHash,
          record.metadata.modelId,
          Date.now(),
          extra
        );
      }
    });
    try {
      tx(records);
    } catch (err) {
      log.error("SqliteVecStore.upsert failed", {
        count: records.length,
        error: err instanceof Error ? err.message : String(err)
      });
      throw err;
    }
  }
  async search(queryVector, limit, filter) {
    if (limit <= 0) return [];
    const queryBuf = Buffer.from(
      queryVector.buffer,
      queryVector.byteOffset,
      queryVector.byteLength
    );
    const knnLimit = filter ? Math.max(limit * 5, 50) : limit;
    const candidateStmt = this.db.prepare(`
      SELECT rowid, distance
      FROM vec_documents
      WHERE embedding MATCH ?
      ORDER BY distance
      LIMIT ?
    `);
    let candidates;
    try {
      candidates = candidateStmt.all(queryBuf, knnLimit);
    } catch (err) {
      log.error("SqliteVecStore.search KNN failed", {
        error: err instanceof Error ? err.message : String(err)
      });
      throw err;
    }
    if (candidates.length === 0) return [];
    const rowids = candidates.map((c) => c.rowid);
    const placeholders = rowids.map(() => "?").join(",");
    const metaRows = this.db.prepare(`
        SELECT m.rowid AS rowid, m.external_id AS external_id,
               meta.url AS url, meta.content_hash AS content_hash,
               meta.model_id AS model_id, meta.extra_json AS extra_json
        FROM vec_id_map m
        JOIN vec_metadata meta ON m.rowid = meta.rowid
        WHERE m.rowid IN (${placeholders})
      `).all(...rowids);
    const metaByRowid = /* @__PURE__ */ new Map();
    for (const r of metaRows) metaByRowid.set(r.rowid, r);
    const results = [];
    for (const cand of candidates) {
      const meta = metaByRowid.get(cand.rowid);
      if (!meta) continue;
      const extra = meta.extra_json ? JSON.parse(meta.extra_json) : void 0;
      const metadata = {
        url: meta.url,
        contentHash: meta.content_hash,
        modelId: meta.model_id,
        ...extra ? { extra } : {}
      };
      if (filter && !matchesFilter(metadata, filter)) continue;
      results.push({
        id: meta.external_id,
        score: 1 / (1 + cand.distance),
        metadata
      });
      if (results.length >= limit) break;
    }
    return results;
  }
  async delete(ids) {
    if (ids.length === 0) return;
    const tx = this.db.transaction((items) => {
      for (const id of items) {
        const existing = this.upsertSelectStmt.get(id);
        if (!existing) continue;
        this.deleteDocStmt.run(BigInt(existing.rowid));
        this.deleteIdMapStmt.run(id);
      }
    });
    try {
      tx(ids);
    } catch (err) {
      log.error("SqliteVecStore.delete failed", {
        count: ids.length,
        error: err instanceof Error ? err.message : String(err)
      });
      throw err;
    }
  }
  async size() {
    const row = this.sizeStmt.get();
    return row.c;
  }
}
function matchesFilter(meta, filter) {
  if (filter.url !== void 0 && meta.url !== filter.url) return false;
  if (filter.contentHash !== void 0 && meta.contentHash !== filter.contentHash) return false;
  if (filter.modelId !== void 0 && meta.modelId !== filter.modelId) return false;
  if (filter.extra !== void 0) {
    const have = meta.extra ?? {};
    for (const [k, v] of Object.entries(filter.extra)) {
      if (have[k] !== v) return false;
    }
  }
  return true;
}
export {
  SqliteVecStore
};
//# sourceMappingURL=sqlite-vec-store.js.map