import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { existsSync } from "fs";
import { join } from "path";
import { homedir } from "os";
class DaemonMaintenanceService {
  config;
  state;
  embeddingProvider = null;
  intervalId;
  isRunning = false;
  onLog;
  constructor(config, onLog) {
    this.config = config;
    this.onLog = onLog;
    this.state = {
      lastRunTime: 0,
      lastFtsRebuild: 0,
      lastVacuum: 0,
      staleFramesCleaned: 0,
      embeddingsGenerated: 0,
      embeddingsTotal: 0,
      embeddingsRemaining: 0,
      ftsRebuilds: 0,
      framesGarbageCollected: 0,
      lastGcRun: 0,
      errors: []
    };
  }
  start() {
    if (this.isRunning || !this.config.enabled) {
      return;
    }
    this.isRunning = true;
    const intervalMs = this.config.interval * 60 * 1e3;
    this.onLog("INFO", "Maintenance service started", {
      interval: this.config.interval,
      staleThresholdDays: this.config.staleFrameThresholdDays,
      ftsRebuildInterval: this.config.ftsRebuildInterval,
      vacuumInterval: this.config.vacuumInterval
    });
    this.intervalId = setInterval(() => {
      this.runMaintenance().catch((err) => {
        this.addError(
          `Maintenance cycle failed: ${err instanceof Error ? err.message : String(err)}`
        );
      });
    }, intervalMs);
  }
  stop() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = void 0;
    }
    this.isRunning = false;
    this.onLog("INFO", "Maintenance service stopped");
  }
  getState() {
    return { ...this.state };
  }
  updateConfig(config) {
    const wasRunning = this.isRunning;
    if (wasRunning) {
      this.stop();
    }
    this.config = { ...this.config, ...config };
    if (wasRunning && this.config.enabled) {
      this.start();
    }
  }
  /**
   * Force-run all maintenance tasks immediately
   */
  async forceRun() {
    await this.runMaintenance();
  }
  /**
   * Run all maintenance tasks in sequence
   */
  async runMaintenance() {
    const startTime = Date.now();
    this.onLog("INFO", "Starting maintenance cycle");
    try {
      const db = await this.getDatabase();
      if (!db) {
        this.onLog("WARN", "No database available for maintenance");
        return;
      }
      await this.cleanStaleFrames(db);
      await this.maybeRebuildFts(db);
      await this.backfillEmbeddings(db);
      await this.maybeVacuum(db);
      await this.generateMissingDigests(db);
      await this.runGC(db);
      await db.disconnect();
      this.state.lastRunTime = Date.now();
      this.onLog("INFO", "Maintenance cycle completed", {
        durationMs: Date.now() - startTime,
        staleFramesCleaned: this.state.staleFramesCleaned,
        ftsRebuilds: this.state.ftsRebuilds,
        embeddingsGenerated: this.state.embeddingsGenerated
      });
    } catch (err) {
      const errorMsg = err instanceof Error ? err.message : String(err);
      this.addError(errorMsg);
      this.onLog("ERROR", "Maintenance cycle failed", { error: errorMsg });
    }
  }
  async cleanStaleFrames(db) {
    try {
      const thresholdSec = Math.floor(Date.now() / 1e3) - this.config.staleFrameThresholdDays * 24 * 3600;
      const rawDb = db.getRawDatabase?.();
      if (!rawDb) return;
      const result = rawDb.prepare(
        "UPDATE frames SET state = 'stale' WHERE state = 'active' AND created_at < ?"
      ).run(thresholdSec);
      const cleaned = result.changes || 0;
      this.state.staleFramesCleaned += cleaned;
      if (cleaned > 0) {
        this.onLog("INFO", `Marked ${cleaned} stale frames`);
      }
    } catch (err) {
      this.addError(
        `Stale frame cleanup: ${err instanceof Error ? err.message : String(err)}`
      );
    }
  }
  async maybeRebuildFts(db) {
    try {
      const hoursSinceLastRebuild = (Date.now() - this.state.lastFtsRebuild) / (1e3 * 3600);
      if (hoursSinceLastRebuild < this.config.ftsRebuildInterval) return;
      if (typeof db.rebuildFtsIndex === "function") {
        await db.rebuildFtsIndex();
        this.state.lastFtsRebuild = Date.now();
        this.state.ftsRebuilds++;
        this.onLog("INFO", "FTS index rebuilt");
      }
    } catch (err) {
      this.addError(
        `FTS rebuild: ${err instanceof Error ? err.message : String(err)}`
      );
    }
  }
  async backfillEmbeddings(db) {
    try {
      if (typeof db.getFramesMissingEmbeddings !== "function") return;
      if (!db.getFeatures?.().supportsVectorSearch) return;
      if (!this.embeddingProvider) return;
      let sinceRowid;
      if (typeof db.getMaintenanceState === "function") {
        const lastId = await db.getMaintenanceState(
          "embedding_backfill_last_id"
        );
        if (lastId != null) {
          sinceRowid = parseInt(lastId, 10);
          if (isNaN(sinceRowid)) sinceRowid = void 0;
        }
      }
      const frames = await db.getFramesMissingEmbeddings(
        this.config.embeddingBatchSize,
        sinceRowid
      );
      if (typeof db.getMaintenanceState === "function") {
        const totalStr = await db.getMaintenanceState(
          "embedding_backfill_total"
        );
        const completedStr = await db.getMaintenanceState(
          "embedding_backfill_completed"
        );
        if (totalStr != null)
          this.state.embeddingsTotal = parseInt(totalStr, 10) || 0;
        if (completedStr != null) {
          const completed = parseInt(completedStr, 10) || 0;
          this.state.embeddingsRemaining = Math.max(
            0,
            this.state.embeddingsTotal - completed
          );
        }
      }
      if (frames.length === 0) return;
      if (this.state.embeddingsTotal === 0 && typeof db.setMaintenanceState === "function") {
        const rawDb = db.getRawDatabase?.();
        if (rawDb) {
          const countResult = rawDb.prepare(
            `SELECT COUNT(*) as count FROM frames f
             LEFT JOIN frame_embeddings ve ON f.frame_id = ve.frame_id
             WHERE ve.frame_id IS NULL`
          ).get();
          this.state.embeddingsTotal = countResult.count;
          await db.setMaintenanceState(
            "embedding_backfill_total",
            String(this.state.embeddingsTotal)
          );
        }
      }
      this.onLog("INFO", `Generating embeddings for ${frames.length} frames`);
      let generated = 0;
      let lastRowid;
      for (const frame of frames) {
        try {
          const text = [
            frame.name,
            frame.digest_text,
            JSON.stringify(frame.inputs)
          ].filter(Boolean).join(" ");
          if (!text.trim()) continue;
          const embedding = await this.embeddingProvider.embed(text);
          await db.storeEmbedding(frame.frame_id, embedding);
          generated++;
          const rawDb = db.getRawDatabase?.();
          if (rawDb) {
            const rowidRow = rawDb.prepare("SELECT rowid FROM frames WHERE frame_id = ?").get(frame.frame_id);
            if (rowidRow) lastRowid = rowidRow.rowid;
          }
        } catch (err) {
          this.addError(
            `Embedding frame ${frame.frame_id}: ${err instanceof Error ? err.message : String(err)}`
          );
        }
      }
      if (typeof db.setMaintenanceState === "function") {
        if (lastRowid != null) {
          await db.setMaintenanceState(
            "embedding_backfill_last_id",
            String(lastRowid)
          );
        }
        const prevCompleted = await db.getMaintenanceState(
          "embedding_backfill_completed"
        );
        const totalCompleted = (parseInt(prevCompleted ?? "0", 10) || 0) + generated;
        await db.setMaintenanceState(
          "embedding_backfill_completed",
          String(totalCompleted)
        );
        this.state.embeddingsRemaining = Math.max(
          0,
          this.state.embeddingsTotal - totalCompleted
        );
      }
      this.state.embeddingsGenerated += generated;
      this.onLog("INFO", `Generated ${generated} embeddings`);
    } catch (err) {
      this.addError(
        `Embedding backfill: ${err instanceof Error ? err.message : String(err)}`
      );
    }
  }
  async maybeVacuum(db) {
    try {
      const hoursSinceLastVacuum = (Date.now() - this.state.lastVacuum) / (1e3 * 3600);
      if (hoursSinceLastVacuum < this.config.vacuumInterval) return;
      const rawDb = db.getRawDatabase?.();
      if (!rawDb) return;
      rawDb.pragma("optimize");
      rawDb.pragma("vacuum");
      this.state.lastVacuum = Date.now();
      this.onLog("INFO", "Database optimized and vacuumed");
    } catch (err) {
      this.addError(
        `VACUUM: ${err instanceof Error ? err.message : String(err)}`
      );
    }
  }
  async generateMissingDigests(db) {
    try {
      const rawDb = db.getRawDatabase?.();
      if (!rawDb) return;
      const count = rawDb.prepare(
        "SELECT COUNT(*) as count FROM frames WHERE digest_text IS NULL AND state = 'active'"
      ).get().count;
      if (count > 0) {
        this.onLog("INFO", `Found ${count} frames missing digest_text`);
      }
    } catch (err) {
      this.addError(
        `Digest generation: ${err instanceof Error ? err.message : String(err)}`
      );
    }
  }
  async runGC(db) {
    try {
      if (this.config.gcEnabled === false) return;
      if (typeof db.runGC !== "function") return;
      const result = await db.runGC({
        retentionDays: this.config.gcRetentionDays ?? 90,
        batchSize: this.config.gcBatchSize ?? 100,
        dryRun: false
      });
      this.state.framesGarbageCollected += result.framesDeleted;
      this.state.lastGcRun = Date.now();
      if (result.framesDeleted > 0) {
        this.onLog(
          "INFO",
          `GC deleted ${result.framesDeleted} expired frames`,
          {
            eventsDeleted: result.eventsDeleted,
            anchorsDeleted: result.anchorsDeleted,
            embeddingsDeleted: result.embeddingsDeleted
          }
        );
      }
    } catch (err) {
      this.addError(`GC: ${err instanceof Error ? err.message : String(err)}`);
    }
  }
  async getDatabase() {
    try {
      const { SQLiteAdapter } = await import("../../core/database/sqlite-adapter.js");
      const { EmbeddingProviderFactory } = await import("../../core/database/embedding-provider-factory.js");
      const dbPath = this.findDatabasePath();
      if (!dbPath) {
        this.onLog("WARN", "No database found for maintenance");
        return null;
      }
      if (!this.embeddingProvider) {
        this.embeddingProvider = await EmbeddingProviderFactory.create({
          provider: this.config.embeddingProvider ?? "transformers",
          model: this.config.embeddingModel,
          dimension: this.config.embeddingDimension,
          apiKey: this.config.embeddingApiKey,
          baseUrl: this.config.embeddingBaseUrl,
          fallbackProviders: this.config.embeddingFallbackProviders
        });
      }
      const adapter = new SQLiteAdapter("maintenance", {
        dbPath,
        embeddingProvider: this.embeddingProvider ?? void 0
      });
      await adapter.connect();
      await adapter.initializeSchema();
      return adapter;
    } catch (err) {
      this.addError(
        `Database init: ${err instanceof Error ? err.message : String(err)}`
      );
      return null;
    }
  }
  findDatabasePath() {
    const homeDir = homedir();
    const candidates = [
      join(process.cwd(), ".stackmemory", "stackmemory.db"),
      join(homeDir, ".stackmemory", "stackmemory.db")
    ];
    for (const candidate of candidates) {
      if (existsSync(candidate)) {
        return candidate;
      }
    }
    return null;
  }
  addError(msg) {
    this.state.errors.push(msg);
    if (this.state.errors.length > 10) {
      this.state.errors = this.state.errors.slice(-10);
    }
  }
}
export {
  DaemonMaintenanceService
};
//# sourceMappingURL=maintenance-service.js.map
