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;
  constructor(projectId, config) {
    super(projectId, config);
    this.dbPath = config.dbPath;
  }
  getFeatures() {
    return {
      supportsFullTextSearch: false,
      // Could enable with FTS5
      supportsVectorSearch: false,
      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");
    }
    if (config.busyTimeout) {
      this.db.pragma(`busy_timeout = ${config.busyTimeout}`);
    }
    if (config.cacheSize) {
      this.db.pragma(`cache_size = ${config.cacheSize}`);
    }
    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);

      -- Set initial schema version if not exists
      INSERT OR IGNORE INTO schema_version (version) VALUES (1);
    `);
    try {
      this.ensureCascadeConstraints();
    } catch (e) {
      logger.warn("Failed to ensure cascade constraints", e);
    }
  }
  /**
   * 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");
  }
  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);
  }
  // Limited search (basic LIKE queries)
  async search(options) {
    if (!this.db)
      throw new DatabaseError(
        "Database not connected",
        ErrorCode.DB_CONNECTION_FAILED
      );
    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 ?
      ORDER BY score DESC
    `;
    const params = Array(6).fill(`%${options.query}%`);
    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) {
    logger.warn("Vector search not supported in SQLite adapter");
    return [];
  }
  async searchHybrid(textQuery, _embedding, weights) {
    return this.search({ query: textQuery, ...weights });
  }
  // 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)
    }));
  }
  // 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
