import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import Database from "better-sqlite3";
import {
  FeatureAwareDatabaseAdapter
} from "./database-adapter.js";
import { logger } from "../monitoring/logger.js";
import { DatabaseError, ErrorCode, ValidationError } from "../errors/index.js";
import * as fs from "fs/promises";
import * as path from "path";
class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
  db = null;
  dbPath;
  inTransactionFlag = false;
  ftsEnabled = false;
  vecEnabled = false;
  embeddingProvider;
  embeddingDimension;
  constructor(projectId, config) {
    super(projectId, config);
    this.dbPath = config.dbPath;
    this.embeddingProvider = config.embeddingProvider;
    this.embeddingDimension = config.embeddingDimension || 384;
  }
  getEmbeddingProvider() {
    return this.embeddingProvider;
  }
  getFeatures() {
    return {
      supportsFullTextSearch: this.ftsEnabled,
      supportsVectorSearch: this.vecEnabled,
      supportsPartitioning: false,
      supportsAnalytics: false,
      supportsCompression: false,
      supportsMaterializedViews: false,
      supportsParallelQueries: false
    };
  }
  async connect() {
    if (this.db) return;
    const config = this.config;
    const dir = path.dirname(this.dbPath);
    await fs.mkdir(dir, { recursive: true });
    this.db = new Database(this.dbPath);
    this.db.pragma("foreign_keys = ON");
    if (config.walMode !== false) {
      this.db.pragma("journal_mode = WAL");
    }
    this.db.pragma("mmap_size = 268435456");
    if (config.busyTimeout) {
      this.db.pragma(`busy_timeout = ${config.busyTimeout}`);
    }
    if (config.cacheSize) {
      this.db.pragma(`cache_size = ${config.cacheSize}`);
    } else {
      this.db.pragma("cache_size = -64000");
    }
    if (config.synchronous) {
      this.db.pragma(`synchronous = ${config.synchronous}`);
    }
    logger.info("SQLite database connected", { dbPath: this.dbPath });
  }
  async disconnect() {
    if (!this.db) return;
    this.db.close();
    this.db = null;
    logger.info("SQLite database disconnected");
  }
  /**
   * Get raw database handle for testing purposes
   * @internal
   */
  getRawDatabase() {
    return this.db;
  }
  isConnected() {
    return this.db !== null && this.db.open;
  }
  async ping() {
    if (!this.db) return false;
    try {
      this.db.prepare("SELECT 1").get();
      return true;
    } catch (error) {
      logger.debug("Database ping failed", {
        error: error instanceof Error ? error.message : String(error)
      });
      return false;
    }
  }
  async initializeSchema() {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS frames (
        frame_id TEXT PRIMARY KEY,
        run_id TEXT NOT NULL,
        project_id TEXT NOT NULL,
        parent_frame_id TEXT REFERENCES frames(frame_id),
        depth INTEGER NOT NULL DEFAULT 0,
        type TEXT NOT NULL,
        name TEXT NOT NULL,
        state TEXT DEFAULT 'active',
        inputs TEXT DEFAULT '{}',
        outputs TEXT DEFAULT '{}',
        digest_text TEXT,
        digest_json TEXT DEFAULT '{}',
        created_at INTEGER DEFAULT (unixepoch()),
        closed_at INTEGER
      );

      CREATE TABLE IF NOT EXISTS events (
        event_id TEXT PRIMARY KEY,
        run_id TEXT NOT NULL,
        frame_id TEXT NOT NULL,
        seq INTEGER NOT NULL,
        event_type TEXT NOT NULL,
        payload TEXT NOT NULL,
        ts INTEGER DEFAULT (unixepoch()),
        FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE
      );

      CREATE TABLE IF NOT EXISTS anchors (
        anchor_id TEXT PRIMARY KEY,
        frame_id TEXT NOT NULL,
        project_id TEXT NOT NULL,
        type TEXT NOT NULL,
        text TEXT NOT NULL,
        priority INTEGER DEFAULT 0,
        created_at INTEGER DEFAULT (unixepoch()),
        metadata TEXT DEFAULT '{}',
        FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE
      );

      CREATE TABLE IF NOT EXISTS schema_version (
        version INTEGER PRIMARY KEY,
        applied_at INTEGER DEFAULT (unixepoch())
      );

      -- Indexes for performance
      CREATE INDEX IF NOT EXISTS idx_frames_run ON frames(run_id);
      CREATE INDEX IF NOT EXISTS idx_frames_project ON frames(project_id);
      CREATE INDEX IF NOT EXISTS idx_frames_parent ON frames(parent_frame_id);
      CREATE INDEX IF NOT EXISTS idx_frames_state ON frames(state);
      CREATE INDEX IF NOT EXISTS idx_frames_created ON frames(created_at DESC);
      CREATE INDEX IF NOT EXISTS idx_events_frame ON events(frame_id);
      CREATE INDEX IF NOT EXISTS idx_events_seq ON events(frame_id, seq);
      CREATE INDEX IF NOT EXISTS idx_anchors_frame ON anchors(frame_id);

      -- Composite index for project-scoped time queries (most common access pattern)
      CREATE INDEX IF NOT EXISTS idx_frames_project_created ON frames(project_id, created_at DESC);

      -- Note: frame_embeddings is a vec0 virtual table with frame_id as PRIMARY KEY;
      -- vec0 handles its own indexing so no CREATE INDEX needed.

      CREATE TABLE IF NOT EXISTS retrieval_log (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        query_text TEXT NOT NULL,
        strategy TEXT NOT NULL,
        results_count INTEGER NOT NULL,
        top_score REAL,
        latency_ms INTEGER NOT NULL,
        result_frame_ids TEXT,
        created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
      );

      CREATE INDEX IF NOT EXISTS idx_retrieval_log_created ON retrieval_log(created_at);

      CREATE TABLE IF NOT EXISTS maintenance_state (
        key TEXT PRIMARY KEY,
        value TEXT NOT NULL,
        updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
      );

      CREATE TABLE IF NOT EXISTS project_registry (
        project_id TEXT PRIMARY KEY,
        repo_path TEXT NOT NULL,
        display_name TEXT,
        db_path TEXT NOT NULL,
        is_active INTEGER DEFAULT 0,
        created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
        last_accessed INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
      );
      CREATE INDEX IF NOT EXISTS idx_project_registry_active ON project_registry(is_active);

      -- Set initial schema version if not exists
      INSERT OR IGNORE INTO schema_version (version) VALUES (1);
    `);
    try {
      this.db.exec(
        "ALTER TABLE frames ADD COLUMN retention_policy TEXT DEFAULT 'default'"
      );
      logger.info("Added retention_policy column to frames");
    } catch {
    }
    this.db.exec(
      "CREATE INDEX IF NOT EXISTS idx_frames_retention_created ON frames(retention_policy, created_at)"
    );
    try {
      this.ensureCascadeConstraints();
    } catch (e) {
      logger.warn("Failed to ensure cascade constraints", e);
    }
    this.initializeFts();
    this.initializeVec();
  }
  /**
   * Ensure ON DELETE CASCADE exists for events/anchors referencing frames
   * Migrates existing tables in-place if needed without data loss.
   */
  ensureCascadeConstraints() {
    if (!this.db) return;
    const needsCascade = (table) => {
      const rows = this.db.prepare(
        `PRAGMA foreign_key_list(${table})`
      ).all();
      return rows.some(
        (r) => r.table === "frames" && String(r.on_delete).toUpperCase() !== "CASCADE"
      );
    };
    const migrateTable = (table) => {
      const createSql = table === "events" ? `CREATE TABLE events_new (
              event_id TEXT PRIMARY KEY,
              run_id TEXT NOT NULL,
              frame_id TEXT NOT NULL,
              seq INTEGER NOT NULL,
              event_type TEXT NOT NULL,
              payload TEXT NOT NULL,
              ts INTEGER DEFAULT (unixepoch()),
              FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE
            );` : `CREATE TABLE anchors_new (
              anchor_id TEXT PRIMARY KEY,
              frame_id TEXT NOT NULL,
              project_id TEXT NOT NULL,
              type TEXT NOT NULL,
              text TEXT NOT NULL,
              priority INTEGER DEFAULT 0,
              created_at INTEGER DEFAULT (unixepoch()),
              metadata TEXT DEFAULT '{}',
              FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE
            );`;
      const cols = table === "events" ? "event_id, run_id, frame_id, seq, event_type, payload, ts" : "anchor_id, frame_id, project_id, type, text, priority, created_at, metadata";
      const idxSql = table === "events" ? [
        "CREATE INDEX IF NOT EXISTS idx_events_frame ON events(frame_id);",
        "CREATE INDEX IF NOT EXISTS idx_events_seq ON events(frame_id, seq);"
      ] : [
        "CREATE INDEX IF NOT EXISTS idx_anchors_frame ON anchors(frame_id);"
      ];
      this.db.exec("PRAGMA foreign_keys = OFF;");
      this.db.exec("BEGIN;");
      this.db.exec(createSql);
      this.db.prepare(
        `INSERT INTO ${table === "events" ? "events_new" : "anchors_new"} (${cols}) SELECT ${cols} FROM ${table}`
      ).run();
      this.db.exec(`DROP TABLE ${table};`);
      this.db.exec(`ALTER TABLE ${table}_new RENAME TO ${table};`);
      for (const stmt of idxSql) this.db.exec(stmt);
      this.db.exec("COMMIT;");
      this.db.exec("PRAGMA foreign_keys = ON;");
      logger.info(`Migrated ${table} to include ON DELETE CASCADE`);
    };
    if (needsCascade("events")) migrateTable("events");
    if (needsCascade("anchors")) migrateTable("anchors");
  }
  /**
   * Initialize FTS5 virtual table and sync triggers
   */
  initializeFts() {
    if (!this.db) return;
    try {
      this.db.exec(`
        CREATE VIRTUAL TABLE IF NOT EXISTS frames_fts USING fts5(
          name, digest_text, inputs, outputs,
          content='frames', content_rowid='rowid'
        );
      `);
      this.db.exec(`
        CREATE TRIGGER IF NOT EXISTS frames_ai AFTER INSERT ON frames BEGIN
          INSERT INTO frames_fts(rowid, name, digest_text, inputs, outputs)
          VALUES (new.rowid, new.name, new.digest_text, new.inputs, new.outputs);
        END;
      `);
      this.db.exec(`
        CREATE TRIGGER IF NOT EXISTS frames_ad AFTER DELETE ON frames BEGIN
          INSERT INTO frames_fts(frames_fts, rowid, name, digest_text, inputs, outputs)
          VALUES ('delete', old.rowid, old.name, old.digest_text, old.inputs, old.outputs);
        END;
      `);
      this.db.exec(`
        CREATE TRIGGER IF NOT EXISTS frames_au AFTER UPDATE ON frames BEGIN
          INSERT INTO frames_fts(frames_fts, rowid, name, digest_text, inputs, outputs)
          VALUES ('delete', old.rowid, old.name, old.digest_text, old.inputs, old.outputs);
          INSERT INTO frames_fts(rowid, name, digest_text, inputs, outputs)
          VALUES (new.rowid, new.name, new.digest_text, new.inputs, new.outputs);
        END;
      `);
      this.migrateToFts();
      this.ftsEnabled = true;
      logger.info("FTS5 full-text search initialized");
    } catch (e) {
      logger.warn(
        "FTS5 initialization failed, falling back to LIKE search",
        e
      );
      this.ftsEnabled = false;
    }
  }
  /**
   * One-time migration: populate FTS index from existing frames data
   */
  migrateToFts() {
    if (!this.db) return;
    const version = this.db.prepare("SELECT MAX(version) as version FROM schema_version").get()?.version || 1;
    if (version < 2) {
      this.db.exec(`
        INSERT OR IGNORE INTO frames_fts(rowid, name, digest_text, inputs, outputs)
        SELECT rowid, name, digest_text, inputs, outputs FROM frames;
      `);
      this.db.prepare("INSERT OR REPLACE INTO schema_version (version) VALUES (?)").run(2);
      logger.info(
        "FTS5 index populated from existing frames (migration v1\u2192v2)"
      );
    }
  }
  /**
   * Initialize sqlite-vec for vector search
   */
  initializeVec() {
    if (!this.db || !this.embeddingProvider) return;
    try {
      let sqliteVec;
      try {
        sqliteVec = require("sqlite-vec");
      } catch {
        logger.info("sqlite-vec not installed, vector search disabled");
        return;
      }
      sqliteVec.load(this.db);
      this.db.exec(`
        CREATE VIRTUAL TABLE IF NOT EXISTS frame_embeddings USING vec0(
          frame_id TEXT PRIMARY KEY,
          embedding float[${this.embeddingDimension}]
        );
      `);
      this.vecEnabled = true;
      logger.info("sqlite-vec vector search initialized", {
        dimension: this.embeddingDimension
      });
    } catch (e) {
      logger.warn(
        "sqlite-vec initialization failed, vector search disabled",
        e
      );
      this.vecEnabled = false;
    }
  }
  /**
   * Rebuild the FTS5 index (for maintenance)
   */
  async rebuildFtsIndex() {
    if (!this.db) {
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    }
    if (!this.ftsEnabled) {
      logger.warn("FTS not enabled, skipping rebuild");
      return;
    }
    this.db.exec("INSERT INTO frames_fts(frames_fts) VALUES('rebuild')");
    logger.info("FTS5 index rebuilt");
  }
  /**
   * Incremental garbage collection: delete expired frames and cascade to related tables.
   * Respects retention_policy per frame:
   *   - 'keep_forever': never deleted
   *   - 'default': deleted after retentionDays (default 90)
   *   - 'archive': same as default
   *   - 'ttl_30d': deleted after 30 days
   *   - 'ttl_7d': deleted after 7 days
   */
  async runGC(options = {}) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const retentionDays = options.retentionDays ?? 90;
    const batchSize = options.batchSize ?? 100;
    const dryRun = options.dryRun ?? false;
    const nowSec = Math.floor(Date.now() / 1e3);
    const defaultCutoff = nowSec - retentionDays * 86400;
    const ttl30dCutoff = nowSec - 30 * 86400;
    const ttl7dCutoff = nowSec - 7 * 86400;
    const candidates = this.db.prepare(
      `SELECT frame_id FROM frames
         WHERE (
           (retention_policy IN ('default', 'archive') AND created_at < ?)
           OR (retention_policy = 'ttl_30d' AND created_at < ?)
           OR (retention_policy = 'ttl_7d' AND created_at < ?)
         )
         AND retention_policy != 'keep_forever'
         LIMIT ?`
    ).all(defaultCutoff, ttl30dCutoff, ttl7dCutoff, batchSize);
    const frameIds = candidates.map((r) => r.frame_id);
    if (frameIds.length === 0) {
      return {
        framesDeleted: 0,
        eventsDeleted: 0,
        anchorsDeleted: 0,
        embeddingsDeleted: 0,
        ftsEntriesDeleted: 0
      };
    }
    if (dryRun) {
      const placeholders2 = frameIds.map(() => "?").join(",");
      const eventsCount = this.db.prepare(
        `SELECT COUNT(*) as count FROM events WHERE frame_id IN (${placeholders2})`
      ).get(...frameIds).count;
      const anchorsCount = this.db.prepare(
        `SELECT COUNT(*) as count FROM anchors WHERE frame_id IN (${placeholders2})`
      ).get(...frameIds).count;
      let embeddingsCount = 0;
      if (this.vecEnabled) {
        embeddingsCount = this.db.prepare(
          `SELECT COUNT(*) as count FROM frame_embeddings WHERE frame_id IN (${placeholders2})`
        ).get(...frameIds).count;
      }
      return {
        framesDeleted: frameIds.length,
        eventsDeleted: eventsCount,
        anchorsDeleted: anchorsCount,
        embeddingsDeleted: embeddingsCount,
        ftsEntriesDeleted: frameIds.length
        // FTS has one entry per frame
      };
    }
    const placeholders = frameIds.map(() => "?").join(",");
    let eventsDeleted = 0;
    let anchorsDeleted = 0;
    let embeddingsDeleted = 0;
    this.db.prepare("BEGIN").run();
    try {
      if (this.vecEnabled) {
        const embResult = this.db.prepare(
          `DELETE FROM frame_embeddings WHERE frame_id IN (${placeholders})`
        ).run(...frameIds);
        embeddingsDeleted = embResult.changes;
      }
      const evtResult = this.db.prepare(`DELETE FROM events WHERE frame_id IN (${placeholders})`).run(...frameIds);
      eventsDeleted = evtResult.changes;
      const ancResult = this.db.prepare(`DELETE FROM anchors WHERE frame_id IN (${placeholders})`).run(...frameIds);
      anchorsDeleted = ancResult.changes;
      this.db.prepare(`DELETE FROM frames WHERE frame_id IN (${placeholders})`).run(...frameIds);
      this.db.prepare("COMMIT").run();
    } catch (error) {
      this.db.prepare("ROLLBACK").run();
      throw error;
    }
    logger.info("GC completed", {
      framesDeleted: frameIds.length,
      eventsDeleted,
      anchorsDeleted,
      embeddingsDeleted
    });
    return {
      framesDeleted: frameIds.length,
      eventsDeleted,
      anchorsDeleted,
      embeddingsDeleted,
      ftsEntriesDeleted: frameIds.length
    };
  }
  async migrateSchema(targetVersion) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const currentVersion = await this.getSchemaVersion();
    if (currentVersion >= targetVersion) {
      logger.info("Schema already at target version", {
        currentVersion,
        targetVersion
      });
      return;
    }
    for (let v = currentVersion + 1; v <= targetVersion; v++) {
      logger.info(`Applying migration to version ${v}`);
      this.db.prepare("UPDATE schema_version SET version = ?").run(v);
    }
  }
  async getSchemaVersion() {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    try {
      const result = this.db.prepare("SELECT MAX(version) as version FROM schema_version").get();
      return result?.version || 0;
    } catch (error) {
      logger.debug("Schema version table not found, returning 0", {
        error: error instanceof Error ? error.message : String(error)
      });
      return 0;
    }
  }
  // Frame operations
  async createFrame(frame) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const frameId = frame.frame_id || this.generateId();
    this.db.prepare(
      `
      INSERT INTO frames (
        frame_id, run_id, project_id, parent_frame_id, depth,
        type, name, state, inputs, outputs, digest_text, digest_json
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `
    ).run(
      frameId,
      frame.run_id,
      frame.project_id || this.projectId,
      frame.parent_frame_id || null,
      frame.depth || 0,
      frame.type,
      frame.name,
      frame.state || "active",
      JSON.stringify(frame.inputs || {}),
      JSON.stringify(frame.outputs || {}),
      frame.digest_text || null,
      JSON.stringify(frame.digest_json || {})
    );
    return frameId;
  }
  async getFrame(frameId) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const row = this.db.prepare("SELECT * FROM frames WHERE frame_id = ?").get(frameId);
    if (!row) return null;
    return {
      ...row,
      inputs: JSON.parse(row.inputs || "{}"),
      outputs: JSON.parse(row.outputs || "{}"),
      digest_json: JSON.parse(row.digest_json || "{}")
    };
  }
  async updateFrame(frameId, updates) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const fields = [];
    const values = [];
    if (updates.state !== void 0) {
      fields.push("state = ?");
      values.push(updates.state);
    }
    if (updates.outputs !== void 0) {
      fields.push("outputs = ?");
      values.push(JSON.stringify(updates.outputs));
    }
    if (updates.digest_text !== void 0) {
      fields.push("digest_text = ?");
      values.push(updates.digest_text);
    }
    if (updates.digest_json !== void 0) {
      fields.push("digest_json = ?");
      values.push(JSON.stringify(updates.digest_json));
    }
    if (updates.closed_at !== void 0) {
      fields.push("closed_at = ?");
      values.push(updates.closed_at);
    }
    if (fields.length === 0) return;
    values.push(frameId);
    this.db.prepare(
      `
      UPDATE frames SET ${fields.join(", ")} WHERE frame_id = ?
    `
    ).run(...values);
  }
  async deleteFrame(frameId) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    await this.deleteFrameAnchors(frameId);
    await this.deleteFrameEvents(frameId);
    this.db.prepare("DELETE FROM frames WHERE frame_id = ?").run(frameId);
  }
  async getActiveFrames(runId) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    let query = "SELECT * FROM frames WHERE state = 'active'";
    const params = [];
    if (runId) {
      query += " AND run_id = ?";
      params.push(runId);
    }
    query += " ORDER BY depth ASC, created_at ASC";
    const rows = this.db.prepare(query).all(...params);
    return rows.map((row) => ({
      ...row,
      inputs: JSON.parse(row.inputs || "{}"),
      outputs: JSON.parse(row.outputs || "{}"),
      digest_json: JSON.parse(row.digest_json || "{}")
    }));
  }
  async closeFrame(frameId, outputs) {
    await this.updateFrame(frameId, {
      state: "closed",
      outputs,
      closed_at: Date.now()
    });
  }
  // Event operations
  async createEvent(event) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const eventId = event.event_id || this.generateId();
    this.db.prepare(
      `
      INSERT INTO events (event_id, run_id, frame_id, seq, event_type, payload, ts)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `
    ).run(
      eventId,
      event.run_id,
      event.frame_id,
      event.seq || 0,
      event.event_type,
      JSON.stringify(event.payload || {}),
      event.ts || Date.now()
    );
    return eventId;
  }
  async getFrameEvents(frameId, options) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    let query = "SELECT * FROM events WHERE frame_id = ?";
    query += this.buildOrderByClause(
      options?.orderBy || "seq",
      options?.orderDirection
    );
    query += this.buildLimitClause(options?.limit, options?.offset);
    const rows = this.db.prepare(query).all(frameId);
    return rows.map((row) => ({
      ...row,
      payload: JSON.parse(row.payload || "{}")
    }));
  }
  async deleteFrameEvents(frameId) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.prepare("DELETE FROM events WHERE frame_id = ?").run(frameId);
  }
  // Anchor operations
  async createAnchor(anchor) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const anchorId = anchor.anchor_id || this.generateId();
    this.db.prepare(
      `
      INSERT INTO anchors (anchor_id, frame_id, project_id, type, text, priority, metadata)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `
    ).run(
      anchorId,
      anchor.frame_id,
      anchor.project_id || this.projectId,
      anchor.type,
      anchor.text,
      anchor.priority || 0,
      JSON.stringify(anchor.metadata || {})
    );
    return anchorId;
  }
  async getFrameAnchors(frameId) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const rows = this.db.prepare(
      `
      SELECT * FROM anchors WHERE frame_id = ? 
      ORDER BY priority DESC, created_at ASC
    `
    ).all(frameId);
    return rows.map((row) => ({
      ...row,
      metadata: JSON.parse(row.metadata || "{}")
    }));
  }
  async deleteFrameAnchors(frameId) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.prepare("DELETE FROM anchors WHERE frame_id = ?").run(frameId);
  }
  // Full-text search with FTS5 + BM25 ranking (fallback to LIKE)
  async search(options) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    if (this.ftsEnabled) {
      try {
        return this.searchFts(options);
      } catch (e) {
        logger.debug("FTS search failed, falling back to LIKE", {
          error: e instanceof Error ? e.message : String(e),
          query: options.query
        });
      }
    }
    return this.searchLike(options);
  }
  /**
   * Sanitize user input for FTS5 MATCH queries.
   * - Strips FTS5 operators and special syntax
   * - Wraps individual terms in double quotes for exact matching
   * - Joins with implicit AND
   * - Supports prefix matching when original query ends with *
   */
  sanitizeFtsQuery(query) {
    const wantsPrefix = query.trimEnd().endsWith("*");
    const cleaned = query.replace(/['"(){}[\]^~*\\,]/g, " ").replace(/\b(AND|OR|NOT|NEAR)\b/gi, "").trim();
    const terms = cleaned.split(/\s+/).filter((t) => t.length > 0);
    if (terms.length === 0) return '""';
    const quoted = terms.map((t) => `"${t}"`);
    if (wantsPrefix) {
      quoted[quoted.length - 1] = quoted[quoted.length - 1] + "*";
    }
    return quoted.join(" ");
  }
  /**
   * FTS5 MATCH search with BM25 ranking
   */
  searchFts(options) {
    const sanitizedQuery = this.sanitizeFtsQuery(options.query);
    const boost = options.boost || {};
    const w0 = boost["name"] || 10;
    const w1 = boost["digest_text"] || 5;
    const w2 = boost["inputs"] || 2;
    const w3 = boost["outputs"] || 1;
    const projectFilter = options.projectId ? "AND f.project_id = ?" : "";
    const sql = `
      SELECT f.*, -bm25(frames_fts, ${w0}, ${w1}, ${w2}, ${w3}) as score
      FROM frames_fts fts
      JOIN frames f ON f.rowid = fts.rowid
      WHERE frames_fts MATCH ?
      ${projectFilter}
      ORDER BY score DESC
      LIMIT ? OFFSET ?
    `;
    const limit = options.limit || 50;
    const offset = options.offset || 0;
    const params = [sanitizedQuery];
    if (options.projectId) params.push(options.projectId);
    params.push(limit, offset);
    const rows = this.db.prepare(sql).all(...params);
    return rows.map((row) => ({
      ...row,
      inputs: JSON.parse(row.inputs || "{}"),
      outputs: JSON.parse(row.outputs || "{}"),
      digest_json: JSON.parse(row.digest_json || "{}")
    }));
  }
  /**
   * Fallback LIKE search for when FTS is unavailable
   */
  searchLike(options) {
    const projectFilter = options.projectId ? "AND project_id = ?" : "";
    const sql = `
      SELECT *,
        CASE
          WHEN name LIKE ? THEN 1.0
          WHEN digest_text LIKE ? THEN 0.8
          WHEN inputs LIKE ? THEN 0.6
          ELSE 0.5
        END as score
      FROM frames
      WHERE (name LIKE ? OR digest_text LIKE ? OR inputs LIKE ?)
      ${projectFilter}
      ORDER BY score DESC
    `;
    const likeParam = `%${options.query}%`;
    const params = Array(6).fill(likeParam);
    if (options.projectId) params.push(options.projectId);
    let rows = this.db.prepare(sql).all(...params);
    if (options.scoreThreshold) {
      rows = rows.filter((row) => row.score >= options.scoreThreshold);
    }
    if (options.limit || options.offset) {
      const start = options.offset || 0;
      const end = options.limit ? start + options.limit : rows.length;
      rows = rows.slice(start, end);
    }
    return rows.map((row) => ({
      ...row,
      inputs: JSON.parse(row.inputs || "{}"),
      outputs: JSON.parse(row.outputs || "{}"),
      digest_json: JSON.parse(row.digest_json || "{}")
    }));
  }
  async searchByVector(embedding, options) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    if (!this.vecEnabled) {
      logger.warn("Vector search not available (sqlite-vec not loaded)");
      return [];
    }
    const limit = options?.limit || 20;
    const sql = `
      SELECT f.*, ve.distance as similarity
      FROM frame_embeddings ve
      JOIN frames f ON f.frame_id = ve.frame_id
      WHERE ve.embedding MATCH ?
      ORDER BY ve.distance
      LIMIT ?
    `;
    const rows = this.db.prepare(sql).all(JSON.stringify(embedding), limit);
    return rows.map((row) => ({
      ...row,
      inputs: JSON.parse(row.inputs || "{}"),
      outputs: JSON.parse(row.outputs || "{}"),
      digest_json: JSON.parse(row.digest_json || "{}")
    }));
  }
  async searchHybrid(textQuery, embedding, weights, mergeStrategy) {
    const textResults = await this.search({ query: textQuery, limit: 50 });
    const vecResults = this.vecEnabled ? await this.searchByVector(embedding, { limit: 50 }) : [];
    if (vecResults.length === 0) {
      return textResults;
    }
    if (textResults.length === 0) {
      return this.normalizeVectorOnly(vecResults);
    }
    if (mergeStrategy === "rrf") {
      return this.mergeByRRF(textResults, vecResults);
    }
    return this.mergeByWeightedScore(textResults, vecResults, weights);
  }
  /**
   * Merge text and vector results using min-max normalized weighted scoring.
   * Both score types are scaled to [0, 1] before combining.
   */
  mergeByWeightedScore(textResults, vecResults, weights) {
    const textWeight = weights?.text ?? 0.6;
    const vecWeight = weights?.vector ?? 0.4;
    const scoreMap = /* @__PURE__ */ new Map();
    const textScores = textResults.map((r) => r.score);
    const minText = Math.min(...textScores);
    const maxText = Math.max(...textScores);
    const rangeText = maxText - minText;
    for (const r of textResults) {
      const normalized = rangeText === 0 ? 1 : (r.score - minText) / rangeText;
      scoreMap.set(r.frame_id, { frame: r, score: normalized * textWeight });
    }
    const distances = vecResults.map((r) => r.similarity);
    const minDist = Math.min(...distances);
    const maxDist = Math.max(...distances);
    const rangeDist = maxDist - minDist;
    for (const r of vecResults) {
      const normalized = rangeDist === 0 ? 1 : 1 - (r.similarity - minDist) / rangeDist;
      const existing = scoreMap.get(r.frame_id);
      if (existing) {
        existing.score += normalized * vecWeight;
      } else {
        scoreMap.set(r.frame_id, { frame: r, score: normalized * vecWeight });
      }
    }
    return Array.from(scoreMap.values()).sort((a, b) => b.score - a.score).map(({ frame, score }) => ({ ...frame, score }));
  }
  /**
   * Merge text and vector results using Reciprocal Rank Fusion.
   * Rank-based merging that is immune to score scale differences.
   */
  mergeByRRF(textResults, vecResults, k = 60) {
    const scoreMap = /* @__PURE__ */ new Map();
    for (let rank = 0; rank < textResults.length; rank++) {
      const r = textResults[rank];
      const rrfScore = 1 / (k + rank + 1);
      scoreMap.set(r.frame_id, { frame: r, score: rrfScore });
    }
    for (let rank = 0; rank < vecResults.length; rank++) {
      const r = vecResults[rank];
      const rrfScore = 1 / (k + rank + 1);
      const existing = scoreMap.get(r.frame_id);
      if (existing) {
        existing.score += rrfScore;
      } else {
        scoreMap.set(r.frame_id, { frame: r, score: rrfScore });
      }
    }
    return Array.from(scoreMap.values()).sort((a, b) => b.score - a.score).map(({ frame, score }) => ({ ...frame, score }));
  }
  /**
   * Convert vector-only results (distances) to [0, 1] scores.
   */
  normalizeVectorOnly(vecResults) {
    if (vecResults.length === 0) return [];
    const distances = vecResults.map((r) => r.similarity);
    const minDist = Math.min(...distances);
    const maxDist = Math.max(...distances);
    const range = maxDist - minDist;
    return vecResults.map((r) => ({
      ...r,
      score: range === 0 ? 1 : 1 - (r.similarity - minDist) / range
    }));
  }
  /**
   * Store an embedding for a frame
   */
  async storeEmbedding(frameId, embedding) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    if (!this.vecEnabled) return;
    this.db.prepare(
      "INSERT OR REPLACE INTO frame_embeddings (frame_id, embedding) VALUES (?, ?)"
    ).run(frameId, JSON.stringify(embedding));
  }
  /**
   * Get a maintenance state value by key
   */
  async getMaintenanceState(key) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const row = this.db.prepare("SELECT value FROM maintenance_state WHERE key = ?").get(key);
    return row?.value ?? null;
  }
  /**
   * Set a maintenance state value by key
   */
  async setMaintenanceState(key, value) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.prepare(
      "INSERT OR REPLACE INTO maintenance_state (key, value, updated_at) VALUES (?, ?, ?)"
    ).run(key, value, Date.now());
  }
  /**
   * Get frames that are missing embeddings
   */
  async getFramesMissingEmbeddings(limit = 50, sinceRowid) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const rowidFilter = sinceRowid != null ? "AND f.rowid > ?" : "";
    const sql = `
      SELECT f.* FROM frames f
      LEFT JOIN frame_embeddings ve ON f.frame_id = ve.frame_id
      WHERE ve.frame_id IS NULL ${rowidFilter}
      ORDER BY f.rowid ASC
      LIMIT ?
    `;
    const params = [];
    if (sinceRowid != null) params.push(sinceRowid);
    params.push(limit);
    const rows = this.db.prepare(sql).all(...params);
    return rows.map((row) => ({
      ...row,
      inputs: JSON.parse(row.inputs || "{}"),
      outputs: JSON.parse(row.outputs || "{}"),
      digest_json: JSON.parse(row.digest_json || "{}")
    }));
  }
  // Project registry operations
  async registerProject(project) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.prepare(
      `INSERT OR REPLACE INTO project_registry
         (project_id, repo_path, display_name, db_path, is_active, created_at, last_accessed)
         VALUES (?, ?, ?, ?, 0, ?, ?)`
    ).run(
      project.projectId,
      project.repoPath,
      project.displayName || null,
      project.dbPath,
      Date.now(),
      Date.now()
    );
  }
  async getRegisteredProjects() {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const rows = this.db.prepare("SELECT * FROM project_registry ORDER BY last_accessed DESC").all();
    return rows.map((row) => ({
      projectId: row.project_id,
      repoPath: row.repo_path,
      displayName: row.display_name,
      dbPath: row.db_path,
      isActive: row.is_active === 1,
      createdAt: row.created_at,
      lastAccessed: row.last_accessed
    }));
  }
  async setActiveProject(projectId) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.prepare("UPDATE project_registry SET is_active = 0").run();
    this.db.prepare(
      "UPDATE project_registry SET is_active = 1, last_accessed = ? WHERE project_id = ?"
    ).run(Date.now(), projectId);
  }
  async getActiveProject() {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const row = this.db.prepare(
      "SELECT project_id FROM project_registry WHERE is_active = 1 LIMIT 1"
    ).get();
    return row?.project_id ?? null;
  }
  async removeProject(projectId) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const result = this.db.prepare("DELETE FROM project_registry WHERE project_id = ?").run(projectId);
    return result.changes > 0;
  }
  async touchProject(projectId) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.prepare(
      "UPDATE project_registry SET last_accessed = ? WHERE project_id = ?"
    ).run(Date.now(), projectId);
  }
  // Basic aggregation
  async aggregate(table, options) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const metrics = options.metrics.map(
      (m) => `${m.operation}(${m.field}) AS ${m.alias || `${m.operation}_${m.field}`}`
    ).join(", ");
    let sql = `SELECT ${options.groupBy.join(", ")}, ${metrics} FROM ${table}`;
    sql += ` GROUP BY ${options.groupBy.join(", ")}`;
    if (options.having) {
      const havingClauses = Object.entries(options.having).map(
        ([key, value]) => `${key} ${typeof value === "object" ? value.op : "="} ?`
      );
      sql += ` HAVING ${havingClauses.join(" AND ")}`;
    }
    return this.db.prepare(sql).all(...Object.values(options.having || {}));
  }
  // Pattern detection (basic)
  async detectPatterns(timeRange) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    let sql = `
      SELECT type as pattern, type, COUNT(*) as frequency, MAX(created_at) as last_seen
      FROM frames
    `;
    const params = [];
    if (timeRange) {
      sql += " WHERE created_at >= ? AND created_at <= ?";
      params.push(
        Math.floor(timeRange.start.getTime() / 1e3),
        Math.floor(timeRange.end.getTime() / 1e3)
      );
    }
    sql += " GROUP BY type HAVING COUNT(*) > 1 ORDER BY frequency DESC";
    const rows = this.db.prepare(sql).all(...params);
    return rows.map((row) => ({
      pattern: row.pattern,
      type: row.type,
      frequency: row.frequency,
      lastSeen: new Date(row.last_seen * 1e3)
    }));
  }
  // Retrieval logging
  async logRetrieval(entry) {
    if (!this.db) return;
    try {
      this.db.prepare(
        `INSERT INTO retrieval_log (query_text, strategy, results_count, top_score, latency_ms, result_frame_ids, created_at)
           VALUES (?, ?, ?, ?, ?, ?, ?)`
      ).run(
        entry.queryText,
        entry.strategy,
        entry.resultsCount,
        entry.topScore,
        entry.latencyMs,
        JSON.stringify(entry.resultFrameIds),
        Date.now()
      );
    } catch (e) {
      logger.warn("Failed to log retrieval", e);
    }
  }
  async getRetrievalStats(sinceDays) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const sinceMs = sinceDays ? Date.now() - sinceDays * 24 * 60 * 60 * 1e3 : 0;
    const whereClause = sinceMs ? "WHERE created_at >= ?" : "";
    const params = sinceMs ? [sinceMs] : [];
    const agg = this.db.prepare(
      `SELECT
           COUNT(*) as total_queries,
           COALESCE(AVG(latency_ms), 0) as avg_latency_ms,
           COALESCE(AVG(results_count), 0) as avg_results_count,
           COUNT(CASE WHEN results_count = 0 THEN 1 END) as queries_with_no_results
         FROM retrieval_log ${whereClause}`
    ).get(...params);
    const p95Offset = Math.max(0, Math.round(agg.total_queries * 0.95) - 1);
    const p95Row = agg.total_queries > 0 ? this.db.prepare(
      `SELECT latency_ms FROM retrieval_log ${whereClause}
               ORDER BY latency_ms ASC
               LIMIT 1 OFFSET ?`
    ).get(...params, p95Offset) : void 0;
    const stratRows = this.db.prepare(
      `SELECT strategy, COUNT(*) as count FROM retrieval_log ${whereClause}
         GROUP BY strategy`
    ).all(...params);
    const strategyDistribution = {};
    for (const row of stratRows) {
      strategyDistribution[row.strategy] = row.count;
    }
    return {
      totalQueries: agg.total_queries,
      avgLatencyMs: Math.round(agg.avg_latency_ms * 100) / 100,
      p95LatencyMs: p95Row?.latency_ms ?? 0,
      strategyDistribution,
      avgResultsCount: Math.round(agg.avg_results_count * 100) / 100,
      queriesWithNoResults: agg.queries_with_no_results
    };
  }
  // Bulk operations
  async executeBulk(operations) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    await this.inTransaction(async () => {
      for (const op of operations) {
        switch (op.type) {
          case "insert":
            const insertCols = Object.keys(op.data);
            const insertPlaceholders = insertCols.map(() => "?").join(",");
            this.db.prepare(
              `INSERT INTO ${op.table} (${insertCols.join(",")}) VALUES (${insertPlaceholders})`
            ).run(...Object.values(op.data));
            break;
          case "update":
            const updateSets = Object.keys(op.data).map((k) => `${k} = ?`).join(",");
            const whereClause = this.buildWhereClause(op.where || {});
            this.db.prepare(
              `UPDATE ${op.table} SET ${updateSets} ${whereClause}`
            ).run(...Object.values(op.data), ...Object.values(op.where || {}));
            break;
          case "delete":
            const deleteWhere = this.buildWhereClause(op.where || {});
            this.db.prepare(`DELETE FROM ${op.table} ${deleteWhere}`).run(
              ...Object.values(op.where || {})
            );
            break;
        }
      }
    });
  }
  async vacuum() {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.pragma("vacuum");
    logger.info("SQLite database vacuumed");
  }
  async analyze() {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.pragma("analyze");
    logger.info("SQLite database analyzed");
  }
  // Statistics
  async getStats() {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    const stats = {
      totalFrames: this.db.prepare("SELECT COUNT(*) as count FROM frames").get().count,
      activeFrames: this.db.prepare(
        "SELECT COUNT(*) as count FROM frames WHERE state = 'active'"
      ).get().count,
      totalEvents: this.db.prepare("SELECT COUNT(*) as count FROM events").get().count,
      totalAnchors: this.db.prepare("SELECT COUNT(*) as count FROM anchors").get().count,
      diskUsage: 0
    };
    try {
      const fileStats = await fs.stat(this.dbPath);
      stats.diskUsage = fileStats.size;
    } catch (error) {
      logger.debug("Failed to get database file size", {
        dbPath: this.dbPath,
        error: error instanceof Error ? error.message : String(error)
      });
    }
    return stats;
  }
  async getQueryStats() {
    logger.warn("Query stats not available for SQLite");
    return [];
  }
  // Transaction support
  async beginTransaction() {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.prepare("BEGIN").run();
    this.inTransactionFlag = true;
  }
  async commitTransaction() {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.prepare("COMMIT").run();
    this.inTransactionFlag = false;
  }
  async rollbackTransaction() {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    this.db.prepare("ROLLBACK").run();
    this.inTransactionFlag = false;
  }
  async inTransaction(callback) {
    await this.beginTransaction();
    try {
      await callback(this);
      await this.commitTransaction();
    } catch (error) {
      await this.rollbackTransaction();
      throw error;
    }
  }
  // Export/Import
  async exportData(tables, format) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    if (format !== "json") {
      throw new ValidationError(
        `Format ${format} not supported for SQLite export`,
        ErrorCode.VALIDATION_FAILED,
        { format, supportedFormats: ["json"] }
      );
    }
    const data = {};
    for (const table of tables) {
      data[table] = this.db.prepare(`SELECT * FROM ${table}`).all();
    }
    return Buffer.from(JSON.stringify(data, null, 2));
  }
  async importData(data, format, options) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    if (format !== "json") {
      throw new ValidationError(
        `Format ${format} not supported for SQLite import`,
        ErrorCode.VALIDATION_FAILED,
        { format, supportedFormats: ["json"] }
      );
    }
    const parsed = JSON.parse(data.toString());
    await this.inTransaction(async () => {
      for (const [table, rows] of Object.entries(parsed)) {
        if (options?.truncate) {
          this.db.prepare(`DELETE FROM ${table}`).run();
        }
        for (const row of rows) {
          const cols = Object.keys(row);
          const placeholders = cols.map(() => "?").join(",");
          if (options?.upsert) {
            const updates = cols.map((c) => `${c} = excluded.${c}`).join(",");
            this.db.prepare(
              `INSERT INTO ${table} (${cols.join(",")}) VALUES (${placeholders})
               ON CONFLICT DO UPDATE SET ${updates}`
            ).run(...Object.values(row));
          } else {
            this.db.prepare(
              `INSERT INTO ${table} (${cols.join(",")}) VALUES (${placeholders})`
            ).run(...Object.values(row));
          }
        }
      }
    });
  }
}
export {
  SQLiteAdapter
};
//# sourceMappingURL=sqlite-adapter.js.map
