/**
 * Asset Index Service — the live, in-process drain for image captioning and
 * document-sidecar extraction.
 *
 * P3b shipped the derive/persist/migration logic (`sweepAssetIndex`) but wired
 * it only to a one-shot boot pass. On a long-lived server (Railway tenants run
 * for days/weeks) that meant a freshly-uploaded image stayed filename-only —
 * not content-searchable — until the next restart. This service closes that gap
 * the same way `SessionManager` closes the commit-batching gap: a long-lived
 * object with a periodic timer, an `enqueue()` the upload paths call, a
 * crash-resilient persisted queue, and a `recoverOnBoot()` that drains anything
 * left unfinished.
 *
 * Shape (mirrors SessionManager deliberately so the lifecycle is familiar):
 *  - `enqueue(assetId)` — fired by VectorService.onAssetPending the moment an
 *    indexable IMAGE assets.db row is seeded `pending` (every image-upload path
 *    funnels through ensureInitialAssetIndexState). Just records the assetId.
 *  - periodic `setInterval` tick — drains the queued ids through
 *    `sweepAssetIndex({ onlyAssets })`, bounded + gated exactly as the boot pass.
 *    Idle when the queue is empty, so an idle server does zero vision work.
 *  - persisted queue (`pending-asset-index.json`, atomic tmp+rename) — survives
 *    a crash; `recoverOnBoot()` replays it AND runs the full unfiltered sweep
 *    (which re-derives anything missing/stale in LanceDB regardless of the
 *    queue), so nothing is lost even if the process died mid-drain.
 *  - `shutdown()` — drains what's queued, then tears down the timer.
 *
 * Config (caption model, the `captioning.enabled` gate, the repo list) is
 * resolved FRESH each tick via the injected `resolveConfig` thunk, so flipping
 * the model or the gate in Settings takes effect without a restart — the same
 * live-resolution property VectorService already has for the Voyage key.
 */
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
import { join } from 'path';
import { sweepAssetIndex, } from '../services/asset-index-sweep.js';
const DEFAULT_INTERVAL_MS = 15_000;
export class AssetIndexQueue {
    deps;
    queue = new Set();
    timer = null;
    draining = false;
    pendingPath;
    intervalMs;
    log;
    constructor(deps) {
        this.deps = deps;
        this.intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS;
        this.log = deps.log ?? (() => { });
        this.pendingPath = join(deps.workspaceManager.getWorkspacePath(), '.studiograph', 'pending-asset-index.json');
    }
    /** Start the periodic drain. Idempotent. */
    start() {
        if (this.timer)
            return;
        // Load anything a prior process queued but never drained.
        const persisted = this.loadPersisted();
        if (persisted)
            for (const k of persisted.pending)
                this.queue.add(k);
        this.timer = setInterval(() => { void this.drainTick(); }, this.intervalMs);
        // Node: don't keep the event loop alive solely for this timer.
        this.timer.unref?.();
    }
    /**
     * Record that an image asset needs caption/OCR indexing, keyed by its
     * immutable assetId. Fired by VectorService.onAssetPending once the
     * assets.db image row is written (covers import, manager/editor upload, and
     * presign finalize). Cheap + synchronous; the derive happens on the next tick.
     */
    enqueue(assetId) {
        this.queue.add(assetId);
        this.persist();
    }
    /** Current queue depth — exposed for the health endpoint / tests. */
    get queueDepth() {
        return this.queue.size;
    }
    /**
     * Boot recovery: replay the persisted queue, then run the FULL unfiltered
     * sweep so anything missing/stale in LanceDB (legacy assets, a drain that
     * died mid-flight) is re-derived regardless of whether it's still queued.
     * Fire-and-forget at boot — it can take minutes and must not block listen().
     *
     * Holds the same `draining` guard a tick uses, so the (slow) boot sweep can't
     * run concurrently with a periodic drain and conflict on the LanceDB writer.
     * Only the ids that were queued WHEN THE SWEEP STARTED are removed afterward —
     * uploads that arrive during the minutes-long sweep stay queued for the next
     * tick rather than being silently dropped.
     */
    async recoverOnBoot() {
        const persisted = this.loadPersisted();
        if (persisted)
            for (const k of persisted.pending)
                this.queue.add(k);
        // Wait out any in-flight tick, then claim the guard for the boot sweep.
        while (this.draining)
            await new Promise((r) => setTimeout(r, 50));
        this.draining = true;
        const handled = new Set(this.queue); // snapshot: what the full sweep covers
        try {
            const res = await this.runSweep(undefined);
            // Remove only the snapshot — newer enqueues survive for the next tick.
            for (const k of handled)
                this.queue.delete(k);
            this.persist();
            return res;
        }
        finally {
            this.draining = false;
        }
    }
    /** Drain the currently-queued ids. Re-entrancy guarded. */
    async drainTick() {
        if (this.draining)
            return;
        if (this.queue.size === 0)
            return;
        this.draining = true;
        // Snapshot + clear up-front: ids enqueued DURING the drain land in the next
        // tick rather than being dropped. On failure we re-add the snapshot.
        const batch = new Set(this.queue);
        this.queue.clear();
        this.persist();
        try {
            await this.runSweep(batch);
        }
        catch (err) {
            // Re-queue the batch so a transient failure retries next tick.
            for (const k of batch)
                this.queue.add(k);
            this.persist();
            this.log(`drain failed (re-queued ${batch.size}): ${err instanceof Error ? err.message : err}`);
        }
        finally {
            this.draining = false;
        }
    }
    /** Resolve live config and run the sweep over `onlyAssets` (undefined = all). */
    async runSweep(onlyAssets) {
        const cfg = this.deps.resolveConfig();
        const res = await sweepAssetIndex({
            workspaceManager: this.deps.workspaceManager,
            repos: cfg.repos,
            workspacePath: this.deps.workspaceManager.getWorkspacePath(),
            captionConfig: cfg.captionConfig,
            captioningEnabled: cfg.captioningEnabled,
        }, { onlyAssets, concurrency: this.deps.concurrency, log: this.log, completeFn: this.deps.completeFn });
        if (res.derived > 0 || res.failed > 0 || res.leftPending > 0) {
            this.log(`assets: ${res.derived} indexed, ${res.failed} failed, ${res.leftPending} pending, ${res.skippedUpToDate} up-to-date`);
        }
        return res;
    }
    /** Graceful shutdown — drain what's queued, then stop the timer. */
    async shutdown() {
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
        }
        if (this.queue.size > 0) {
            await this.drainTick();
        }
    }
    persist() {
        const state = {
            version: 1,
            savedAt: new Date().toISOString(),
            pending: Array.from(this.queue),
        };
        try {
            mkdirSync(join(this.deps.workspaceManager.getWorkspacePath(), '.studiograph'), { recursive: true });
            const tmp = `${this.pendingPath}.tmp`;
            writeFileSync(tmp, JSON.stringify(state, null, 2), 'utf-8');
            renameSync(tmp, this.pendingPath);
        }
        catch (err) {
            this.log(`failed to persist asset-index queue: ${err instanceof Error ? err.message : err}`);
        }
    }
    loadPersisted() {
        try {
            if (!existsSync(this.pendingPath))
                return null;
            const parsed = JSON.parse(readFileSync(this.pendingPath, 'utf-8'));
            if (parsed.version !== 1 || !Array.isArray(parsed.pending))
                return null;
            return parsed;
        }
        catch (err) {
            this.log(`failed to load asset-index queue: ${err instanceof Error ? err.message : err}`);
            return null;
        }
    }
}
//# sourceMappingURL=asset-index-queue.js.map