"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var database_exports = {};
__export(database_exports, {
  MemoryDatabase: () => MemoryDatabase
});
module.exports = __toCommonJS(database_exports);
var fs = __toESM(require("fs"));
var path = __toESM(require("path"));
var crypto = __toESM(require("crypto"));
function generateId() {
  return crypto.randomUUID();
}
function hashContent(content) {
  return crypto.createHash("sha256").update(content).digest("hex");
}
class MemoryDatabase {
  constructor(db) {
    this.initialized = false;
    this.db = db;
  }
  /**
   * Apply memory schema. Called once when enableMemory is true.
   */
  initialize() {
    if (this.initialized) return;
    const schemaPath = path.join(__dirname, "../db/memory-schema.sql");
    const sql = fs.readFileSync(schemaPath, "utf8");
    this.db.exec(sql);
    this.initialized = true;
  }
  // ── Sessions ─────────────────────────────────────────────────────────────
  /**
   * Get or create an active session. Auto-creates if no active session exists
   * within the timeout window.
   */
  getOrCreateSession(ide, cwd, timeoutSeconds = 7200) {
    const cutoff = Date.now() - timeoutSeconds * 1e3;
    const existing = this.db.get(
      `SELECT id FROM mem_sessions
       WHERE ended_at IS NULL AND ide = ? AND started_at > ?
       ORDER BY started_at DESC LIMIT 1`,
      [ide, cutoff]
    );
    if (existing) return existing.id;
    this.db.run(
      `UPDATE mem_sessions SET ended_at = ? WHERE ended_at IS NULL AND ide = ?`,
      [Date.now(), ide]
    );
    const id = generateId();
    this.db.run(
      `INSERT INTO mem_sessions (id, ide, cwd, started_at) VALUES (?, ?, ?, ?)`,
      [id, ide, cwd, Date.now()]
    );
    return id;
  }
  endSession(sessionId) {
    this.db.run(
      `UPDATE mem_sessions SET ended_at = ? WHERE id = ? AND ended_at IS NULL`,
      [Date.now(), sessionId]
    );
  }
  getSession(sessionId) {
    const row = this.db.get("SELECT * FROM mem_sessions WHERE id = ?", [sessionId]);
    return row ? this.rowToSession(row) : null;
  }
  listSessions(limit = 10) {
    return this.db.all(
      "SELECT * FROM mem_sessions ORDER BY started_at DESC LIMIT ?",
      [limit]
    ).map(this.rowToSession);
  }
  // ── Observations ─────────────────────────────────────────────────────────
  /**
   * Insert an observation. Returns the ID, or null if a duplicate (same content_hash).
   */
  insertObservation(content, opts = {}) {
    const id = generateId();
    const contentHash = hashContent(content);
    const kind = opts.kind ?? "note";
    const source = opts.source ?? "manual";
    const tags = opts.tags ? JSON.stringify(opts.tags) : null;
    const now = Date.now();
    try {
      this.db.run(
        `INSERT INTO mem_observations (id, session_id, content, content_raw, content_hash, kind, source, tags, created_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        [id, opts.sessionId ?? null, content, opts.contentRaw ?? null, contentHash, kind, source, tags, now]
      );
    } catch (err) {
      if (err.message?.includes("UNIQUE constraint failed") || err.message?.includes("unique")) {
        return null;
      }
      throw err;
    }
    this.db.run(
      `INSERT INTO mem_fts (id, content, kind) VALUES (?, ?, ?)`,
      [id, content, kind]
    );
    return id;
  }
  getObservation(id) {
    const row = this.db.get("SELECT * FROM mem_observations WHERE id = ?", [id]);
    return row ? this.rowToObservation(row) : null;
  }
  getObservationsBySession(sessionId, limit = 50) {
    return this.db.all(
      "SELECT * FROM mem_observations WHERE session_id = ? ORDER BY created_at DESC LIMIT ?",
      [sessionId, limit]
    ).map(this.rowToObservation);
  }
  getObservationsByTimeRange(from, to, limit = 50) {
    return this.db.all(
      "SELECT * FROM mem_observations WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT ?",
      [from, to, limit]
    ).map(this.rowToObservation);
  }
  // ── FTS Search ───────────────────────────────────────────────────────────
  searchFTS(query, opts = {}) {
    const { limit = 10, kind, sessionId } = opts;
    const safe = query.replace(/\b(AND|OR|NOT)\b/g, " ").replace(/['"*()?\-+^~:{}\\\.\/,]/g, " ").replace(/\s+/g, " ").trim();
    if (!safe) return [];
    const ftsQuery = safe + "*";
    const safeLimit = Math.max(1, Math.floor(Number(limit)));
    const conditions = [];
    const params = [];
    if (kind) {
      conditions.push("o.kind = ?");
      params.push(kind);
    }
    if (sessionId) {
      conditions.push("o.session_id = ?");
      params.push(sessionId);
    }
    const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
    const rows = this.db.all(
      `SELECT o.*, rank FROM mem_fts f
       JOIN mem_observations o ON o.id = f.id
       WHERE mem_fts MATCH '${ftsQuery}' ${where}
       ORDER BY rank
       LIMIT ${safeLimit}`,
      params
    );
    return rows.map((row) => ({
      observation: this.rowToObservation(row),
      score: Math.abs(row.rank ?? 0),
      scoreSource: "fts"
    }));
  }
  // ── Links ────────────────────────────────────────────────────────────────
  linkToSymbol(observationId, qualifiedName, relevance = 1) {
    this.db.run(
      `INSERT OR IGNORE INTO mem_links (observation_id, qualified_name, relevance)
       VALUES (?, ?, ?)`,
      [observationId, qualifiedName, relevance]
    );
  }
  linkToSymbols(observationId, qualifiedNames) {
    for (const qn of qualifiedNames) {
      this.linkToSymbol(observationId, qn);
    }
  }
  /**
   * Get observations linked to a symbol by qualified_name.
   * Resolves through mem_links — no dependency on node IDs.
   */
  getLinkedObservations(qualifiedName, limit = 10) {
    const rows = this.db.all(
      `SELECT o.*, l.relevance FROM mem_observations o
       JOIN mem_links l ON l.observation_id = o.id
       WHERE l.qualified_name = ?
       ORDER BY l.relevance DESC, o.created_at DESC
       LIMIT ?`,
      [qualifiedName, limit]
    );
    return rows.map((row) => ({
      observation: this.rowToObservation(row),
      score: row.relevance ?? 1,
      scoreSource: "fts"
    }));
  }
  /**
   * Get observations linked to any of the given qualified names.
   * Used by kirograph_context and kirograph_impact to surface memory.
   */
  getLinkedObservationsForSymbols(qualifiedNames, limit = 5) {
    if (qualifiedNames.length === 0) return [];
    const placeholders = qualifiedNames.map(() => "?").join(",");
    const rows = this.db.all(
      `SELECT DISTINCT o.*, MAX(l.relevance) as relevance FROM mem_observations o
       JOIN mem_links l ON l.observation_id = o.id
       WHERE l.qualified_name IN (${placeholders})
       GROUP BY o.id
       ORDER BY relevance DESC, o.created_at DESC
       LIMIT ?`,
      [...qualifiedNames, limit]
    );
    return rows.map((row) => ({
      observation: this.rowToObservation(row),
      score: row.relevance ?? 1,
      scoreSource: "fts"
    }));
  }
  // ── Vectors ──────────────────────────────────────────────────────────────
  insertVector(observationId, embedding, model) {
    this.db.run(
      `INSERT OR REPLACE INTO mem_vectors (observation_id, embedding, model, created_at)
       VALUES (?, ?, ?, ?)`,
      [observationId, embedding, model, Date.now()]
    );
  }
  getVector(observationId) {
    const row = this.db.get(
      "SELECT embedding, model FROM mem_vectors WHERE observation_id = ?",
      [observationId]
    );
    return row ? { embedding: row.embedding, model: row.model } : null;
  }
  getVectorCount() {
    const row = this.db.get("SELECT COUNT(*) as count FROM mem_vectors");
    return row?.count ?? 0;
  }
  getVectorModelMismatch(currentModel) {
    const row = this.db.get(
      "SELECT COUNT(*) as count FROM mem_vectors WHERE model != ?",
      [currentModel]
    );
    return row?.count ?? 0;
  }
  getAllVectors(model) {
    const sql = model ? "SELECT observation_id, embedding FROM mem_vectors WHERE model = ?" : "SELECT observation_id, embedding FROM mem_vectors";
    const params = model ? [model] : [];
    return this.db.all(sql, params).map((row) => ({
      observationId: row.observation_id,
      embedding: row.embedding
    }));
  }
  deleteVectors() {
    this.db.run("DELETE FROM mem_vectors");
  }
  // ── Stats ────────────────────────────────────────────────────────────────
  getStats(currentModel) {
    const sessions = this.db.get("SELECT COUNT(*) as c FROM mem_sessions")?.c ?? 0;
    const activeSessions = this.db.get("SELECT COUNT(*) as c FROM mem_sessions WHERE ended_at IS NULL")?.c ?? 0;
    const observations = this.db.get("SELECT COUNT(*) as c FROM mem_observations")?.c ?? 0;
    const links = this.db.get("SELECT COUNT(*) as c FROM mem_links")?.c ?? 0;
    const vectors = this.db.get("SELECT COUNT(*) as c FROM mem_vectors")?.c ?? 0;
    const mismatch = currentModel ? this.getVectorModelMismatch(currentModel) : 0;
    return {
      sessions,
      activeSessions,
      observations,
      links,
      vectors,
      embeddableCount: observations,
      modelMismatch: mismatch > 0,
      currentModel
    };
  }
  // ── Maintenance ──────────────────────────────────────────────────────────
  /**
   * Delete observations older than the given timestamp.
   * Cascades to mem_links and mem_vectors via FK.
   */
  prune(olderThan) {
    const ids = this.db.all(
      "SELECT id FROM mem_observations WHERE created_at < ?",
      [olderThan]
    ).map((r) => r.id);
    if (ids.length === 0) return 0;
    const placeholders = ids.map(() => "?").join(",");
    this.db.run(`DELETE FROM mem_fts WHERE id IN (${placeholders})`, ids);
    this.db.run(`DELETE FROM mem_observations WHERE created_at < ?`, [olderThan]);
    this.db.run(
      `DELETE FROM mem_sessions WHERE id NOT IN (SELECT DISTINCT session_id FROM mem_observations WHERE session_id IS NOT NULL)`
    );
    return ids.length;
  }
  /**
   * Find stale links where qualified_name no longer exists in the nodes table.
   */
  findStaleLinks() {
    const rows = this.db.all(
      `SELECT l.* FROM mem_links l
       LEFT JOIN nodes n ON n.qualified_name = l.qualified_name
       WHERE n.id IS NULL`
    );
    return rows.map((row) => ({
      observationId: row.observation_id,
      qualifiedName: row.qualified_name,
      relevance: row.relevance
    }));
  }
  /**
   * Remove stale links (where qualified_name no longer resolves).
   */
  removeStaleLinks() {
    const result = this.db.run(
      `DELETE FROM mem_links WHERE qualified_name NOT IN (SELECT DISTINCT qualified_name FROM nodes)`
    );
    return result?.changes ?? 0;
  }
  /**
   * Close stale sessions (started but never ended, older than timeout).
   */
  closeStaleSessionsOlderThan(timeoutMs) {
    const cutoff = Date.now() - timeoutMs;
    const result = this.db.run(
      `UPDATE mem_sessions SET ended_at = ? WHERE ended_at IS NULL AND started_at < ?`,
      [Date.now(), cutoff]
    );
    return result?.changes ?? 0;
  }
  // ── Privacy ──────────────────────────────────────────────────────────────
  /**
   * Strip <private>...</private> blocks from text.
   */
  static stripPrivate(text) {
    return text.replace(/<private>[\s\S]*?<\/private>/gi, "").trim();
  }
  // ── Row mappers ──────────────────────────────────────────────────────────
  rowToSession(row) {
    return {
      id: row.id,
      ide: row.ide ?? void 0,
      cwd: row.cwd ?? void 0,
      startedAt: row.started_at,
      endedAt: row.ended_at ?? void 0
    };
  }
  rowToObservation(row) {
    return {
      id: row.id,
      sessionId: row.session_id ?? void 0,
      content: row.content,
      contentRaw: row.content_raw ?? void 0,
      contentHash: row.content_hash,
      kind: row.kind,
      source: row.source,
      tags: row.tags ? JSON.parse(row.tags) : void 0,
      createdAt: row.created_at
    };
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  MemoryDatabase
});
//# sourceMappingURL=database.js.map
