import { createLogger } from "../../logger.js";
const log = createLogger("cache");
const MIGRATION_001_SQLITE_VEC = `
CREATE VIRTUAL TABLE IF NOT EXISTS vec_documents USING vec0(
  embedding float[384]
);

CREATE TABLE IF NOT EXISTS vec_id_map (
  rowid INTEGER PRIMARY KEY AUTOINCREMENT,
  external_id TEXT NOT NULL UNIQUE
);

CREATE TABLE IF NOT EXISTS vec_metadata (
  rowid INTEGER PRIMARY KEY REFERENCES vec_id_map(rowid) ON DELETE CASCADE,
  url TEXT NOT NULL,
  content_hash TEXT NOT NULL,
  model_id TEXT NOT NULL,
  created_at INTEGER NOT NULL,
  extra_json TEXT
);

CREATE INDEX IF NOT EXISTS idx_vec_metadata_url ON vec_metadata(url);
CREATE INDEX IF NOT EXISTS idx_vec_metadata_hash ON vec_metadata(content_hash);
CREATE INDEX IF NOT EXISTS idx_vec_metadata_model ON vec_metadata(model_id);
`;
const MIGRATION_002_FEED_ITEMS = `
CREATE TABLE IF NOT EXISTS feed_items (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  feed_url TEXT NOT NULL,
  guid TEXT NOT NULL,
  title TEXT NOT NULL,
  link TEXT NOT NULL,
  summary TEXT NOT NULL,
  published_date TEXT,
  category TEXT NOT NULL DEFAULT 'news',
  fetched_at TEXT NOT NULL,
  UNIQUE(feed_url, guid)
);

CREATE INDEX IF NOT EXISTS idx_feed_items_published ON feed_items(published_date);
CREATE INDEX IF NOT EXISTS idx_feed_items_feed_url ON feed_items(feed_url);

CREATE VIRTUAL TABLE IF NOT EXISTS feed_items_fts USING fts5(
  title, summary, link UNINDEXED,
  content='feed_items',
  content_rowid='id'
);

CREATE TRIGGER IF NOT EXISTS feed_items_ai AFTER INSERT ON feed_items BEGIN
  INSERT INTO feed_items_fts(rowid, title, summary, link) VALUES (new.id, new.title, new.summary, new.link);
END;

CREATE TRIGGER IF NOT EXISTS feed_items_ad AFTER DELETE ON feed_items BEGIN
  INSERT INTO feed_items_fts(feed_items_fts, rowid, title, summary, link) VALUES('delete', old.id, old.title, old.summary, old.link);
END;

CREATE TRIGGER IF NOT EXISTS feed_items_au AFTER UPDATE ON feed_items BEGIN
  INSERT INTO feed_items_fts(feed_items_fts, rowid, title, summary, link) VALUES('delete', old.id, old.title, old.summary, old.link);
  INSERT INTO feed_items_fts(feed_items_fts, rowid, title, summary, link) VALUES (new.id, new.title, new.summary, new.link);
END;
`;
const MIGRATION_003_CRAWL_ETAGS = `
CREATE TABLE IF NOT EXISTS crawl_etags (
  url TEXT PRIMARY KEY,
  origin TEXT NOT NULL,
  etag TEXT,
  last_modified TEXT,
  fetched_at TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_crawl_etags_origin ON crawl_etags(origin);
`;
const MIGRATION_004_WATCH_JOBS = `
CREATE TABLE IF NOT EXISTS watch_jobs (
  id TEXT PRIMARY KEY,
  url TEXT NOT NULL,
  interval_seconds INTEGER NOT NULL,
  selector TEXT,
  last_check_at INTEGER,
  last_content_hash TEXT,
  status TEXT NOT NULL DEFAULT 'active',
  notification TEXT NOT NULL DEFAULT 'inline',
  created_at INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_watch_jobs_status ON watch_jobs(status);
CREATE INDEX IF NOT EXISTS idx_watch_jobs_url ON watch_jobs(url);
`;
const MIGRATION_005_TLS_ROUTING = `
CREATE TABLE IF NOT EXISTS domain_routing (
  domain TEXT PRIMARY KEY,
  prefer_playwright INTEGER DEFAULT 0,
  http_failures INTEGER DEFAULT 0,
  last_updated TEXT
);
`;
const MIGRATION_006_URL_CACHE_HTTP_STATUS = "";
const MIGRATION_007_DROP_LP_ROUTING = "";
const MIGRATIONS = [
  { name: "001-sqlite-vec", sql: MIGRATION_001_SQLITE_VEC, requiresVec: true },
  { name: "002-feed-items", sql: MIGRATION_002_FEED_ITEMS },
  { name: "003-crawl-etags", sql: MIGRATION_003_CRAWL_ETAGS },
  { name: "004-watch-jobs", sql: MIGRATION_004_WATCH_JOBS },
  {
    name: "005-tls-routing",
    sql: MIGRATION_005_TLS_ROUTING,
    /**
     * Post-step adds the TLS-impersonation columns to domain_routing using
     * pragma table_info to skip already-present columns. SQLite has no
     * `ADD COLUMN IF NOT EXISTS` so we gate at the JS layer to keep the
     * migration idempotent if a column was added out-of-band.
     */
    postStep: (db) => {
      const cols = db.pragma("table_info(domain_routing)");
      const names = new Set(cols.map((c) => c.name));
      if (!names.has("prefer_tls_impersonation")) {
        db.exec("ALTER TABLE domain_routing ADD COLUMN prefer_tls_impersonation INTEGER DEFAULT 0");
      }
      if (!names.has("tls_success_count")) {
        db.exec("ALTER TABLE domain_routing ADD COLUMN tls_success_count INTEGER DEFAULT 0");
      }
    }
  },
  {
    name: "006-url-cache-http-status",
    sql: MIGRATION_006_URL_CACHE_HTTP_STATUS,
    postStep: (db) => {
      const cols = db.pragma("table_info(url_cache)");
      if (cols.length === 0) return;
      const names = new Set(cols.map((c) => c.name));
      if (!names.has("http_status")) {
        db.exec("ALTER TABLE url_cache ADD COLUMN http_status INTEGER");
      }
    }
  },
  {
    name: "007-drop-lp-routing",
    sql: MIGRATION_007_DROP_LP_ROUTING,
    postStep: (db) => {
      const tables = db.prepare(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='lightpanda_routing'"
      ).all();
      if (tables.length > 0) {
        db.exec("DROP TABLE lightpanda_routing");
      }
    }
  }
];
function isReadOnlyError(err) {
  if (!err) return false;
  const code = err.code;
  if (code === "SQLITE_READONLY" || code === "SQLITE_READONLY_DBMOVED") return true;
  const msg = err instanceof Error ? err.message : String(err);
  return /SQLITE_READONLY|attempt to write a readonly|readonly database/i.test(msg);
}
let readOnlyWarned = false;
function _resetMigrationGuard() {
  readOnlyWarned = false;
}
function applyMigrations(db, opts = { vecLoaded: true }) {
  if (readOnlyWarned) {
    return;
  }
  try {
    db.exec(`
      CREATE TABLE IF NOT EXISTS schema_migrations (
        name TEXT PRIMARY KEY,
        applied_at INTEGER NOT NULL
      )
    `);
  } catch (err) {
    if (isReadOnlyError(err)) {
      readOnlyWarned = true;
      log.warn("database is read-only \u2014 skipping migrations for this process", {
        cause: err instanceof Error ? err.message : String(err)
      });
      return;
    }
    throw err;
  }
  const appliedRows = db.prepare("SELECT name FROM schema_migrations").all();
  const applied = new Set(appliedRows.map((r) => r.name));
  const recordStmt = db.prepare("INSERT INTO schema_migrations (name, applied_at) VALUES (?, ?)");
  for (const migration of MIGRATIONS) {
    if (applied.has(migration.name)) continue;
    if (migration.requiresVec && !opts.vecLoaded) {
      log.warn("migration skipped \u2014 sqlite-vec not loaded", { name: migration.name });
      continue;
    }
    try {
      db.transaction(() => {
        db.exec(migration.sql);
        if (migration.postStep) {
          migration.postStep(db);
        }
        recordStmt.run(migration.name, Date.now());
      })();
      log.info("migration applied", { name: migration.name });
    } catch (err) {
      if (isReadOnlyError(err)) {
        readOnlyWarned = true;
        log.warn("database is read-only \u2014 skipping remaining migrations for this process", {
          name: migration.name,
          cause: err instanceof Error ? err.message : String(err)
        });
        return;
      }
      log.error("migration failed", {
        name: migration.name,
        error: err instanceof Error ? err.message : String(err)
      });
      throw err;
    }
  }
}
export {
  MIGRATIONS,
  _resetMigrationGuard,
  applyMigrations
};
//# sourceMappingURL=runner.js.map