import Database from "better-sqlite3";
import { mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { createLogger } from "../logger.js";
import { getConfig } from "../config.js";
import {
  getEmbedProvider
} from "../providers/embed-provider.js";
import {
  getVectorStore
} from "../providers/vector-store.js";
const log = createLogger("embedding");
const DEFAULT_MAX_ATTEMPTS = 3;
class BackgroundIndexQueue {
  db;
  resolveProvider;
  resolveStore;
  maxAttempts;
  syncMode;
  worker = null;
  wake = null;
  stopped = false;
  autoStart;
  inflight = null;
  constructor(opts) {
    mkdirSync(dirname(opts.dbPath), { recursive: true });
    this.db = new Database(opts.dbPath);
    this.db.pragma("journal_mode = WAL");
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS index_jobs (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        url TEXT UNIQUE NOT NULL,
        text TEXT NOT NULL,
        content_hash TEXT NOT NULL,
        attempts INTEGER NOT NULL DEFAULT 0,
        created_at TEXT NOT NULL DEFAULT (datetime('now'))
      );
    `);
    this.resolveProvider = opts.embedProvider ?? getEmbedProvider;
    this.resolveStore = opts.vectorStore ?? getVectorStore;
    this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
    this.syncMode = opts.syncMode ?? false;
    this.autoStart = opts.autoStart ?? true;
    if (this.autoStart && !this.syncMode) {
      this.start();
    }
  }
  enqueue(job) {
    this.db.prepare(
      `INSERT INTO index_jobs (url, text, content_hash, attempts)
         VALUES (?, ?, ?, 0)
         ON CONFLICT(url) DO UPDATE SET
           text = excluded.text,
           content_hash = excluded.content_hash,
           attempts = 0`
    ).run(job.url, job.text, job.contentHash);
    if (this.syncMode) {
      return this.processOne().then(() => void 0);
    }
    this.kickWorker();
    return Promise.resolve();
  }
  pendingSize() {
    const row = this.db.prepare("SELECT COUNT(*) AS c FROM index_jobs").get();
    return row.c;
  }
  start() {
    if (this.worker || this.stopped) return;
    this.worker = this.runLoop().catch((err) => {
      log.error("background index worker crashed", {
        error: err instanceof Error ? err.message : String(err)
      });
    });
  }
  async drain() {
    while (!this.stopped && this.pendingSize() > 0) {
      await this.processOne();
    }
  }
  shutdown() {
    if (this.stopped) return;
    this.stopped = true;
    const wake = this.wake;
    this.wake = null;
    if (wake) wake();
    try {
      this.db.close();
    } catch {
    }
  }
  kickWorker() {
    if (this.stopped || !this.autoStart) return;
    if (!this.worker) this.start();
    const wake = this.wake;
    this.wake = null;
    if (wake) wake();
  }
  async runLoop() {
    while (!this.stopped) {
      const processed = await this.processOne();
      if (processed) continue;
      await new Promise((resolve) => {
        this.wake = resolve;
      });
    }
  }
  async processOne() {
    if (this.stopped) return false;
    if (this.inflight) {
      await this.inflight.catch(() => void 0);
      if (this.stopped) return false;
    }
    let resolveInflight;
    this.inflight = new Promise((r) => {
      resolveInflight = r;
    });
    try {
      const result = await this.processOneInner();
      resolveInflight(result);
      return result;
    } catch (err) {
      resolveInflight(false);
      throw err;
    } finally {
      this.inflight = null;
    }
  }
  async processOneInner() {
    const row = this.db.prepare(
      "SELECT id, url, text, content_hash, attempts FROM index_jobs ORDER BY id ASC LIMIT 1"
    ).get();
    if (!row) return false;
    try {
      const provider = await this.resolveProvider();
      const store = await this.resolveStore();
      const vectors = await provider.embed([row.text]);
      if (vectors.length > 0) {
        const record = {
          id: row.url,
          vector: vectors[0],
          metadata: {
            url: row.url,
            contentHash: row.content_hash,
            modelId: provider.modelId
          }
        };
        await store.upsert([record]);
      }
      this.db.prepare("DELETE FROM index_jobs WHERE id = ?").run(row.id);
      return true;
    } catch (err) {
      const attempts = row.attempts + 1;
      log.warn("background index job failed", {
        url: row.url,
        attempts,
        maxAttempts: this.maxAttempts,
        error: err instanceof Error ? err.message : String(err)
      });
      if (attempts >= this.maxAttempts) {
        this.db.prepare("DELETE FROM index_jobs WHERE id = ?").run(row.id);
      } else {
        this.db.prepare("UPDATE index_jobs SET attempts = ? WHERE id = ?").run(attempts, row.id);
      }
      return true;
    }
  }
}
let singleton = null;
function getBackgroundIndexQueue() {
  if (!singleton) {
    const dbPath = join(getConfig().dataDir, "jobs.db");
    singleton = new BackgroundIndexQueue({
      dbPath,
      syncMode: process.env.WIGOLO_WAIT_FOR_INDEX === "1"
    });
  }
  return singleton;
}
function _resetBackgroundIndexQueueForTest() {
  if (singleton) {
    singleton.shutdown();
    singleton = null;
  }
}
export {
  BackgroundIndexQueue,
  _resetBackgroundIndexQueueForTest,
  getBackgroundIndexQueue
};
//# sourceMappingURL=background-queue.js.map