/**
 * Loop manager — owns in-memory setInterval handles for session-scoped loops.
 *
 * Pure runtime. No I/O, no persistence. State lives in a Map and dies
 * with the process. The onTick callback is injected at construction
 * (wired to session.followUp in main.ts).
 */
import { randomBytes } from 'node:crypto';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Minimum interval to prevent spam. */
const MIN_INTERVAL_MS = 30_000;
/** Maximum concurrent loops. */
const MAX_CONCURRENT = 5;
/** Default safety cap on tick count. */
const DEFAULT_MAX_TICKS = 100;
export class LoopManager {
    entries = new Map();
    onTick;
    changeListeners = [];
    constructor(onTick) {
        this.onTick = onTick;
    }
    /** Register a listener called whenever loops are added, removed, or tick. */
    onChange(fn) {
        this.changeListeners.push(fn);
        return () => {
            const idx = this.changeListeners.indexOf(fn);
            if (idx >= 0)
                this.changeListeners.splice(idx, 1);
        };
    }
    notifyChange() {
        for (const fn of this.changeListeners)
            fn();
    }
    start(opts) {
        if (opts.intervalMs < MIN_INTERVAL_MS) {
            throw new Error(`Minimum interval is ${MIN_INTERVAL_MS}ms (${MIN_INTERVAL_MS / 1000}s)`);
        }
        if (this.entries.size >= MAX_CONCURRENT) {
            throw new Error(`Maximum ${MAX_CONCURRENT} concurrent loops. Stop one first.`);
        }
        const loop = {
            id: randomBytes(3).toString('hex'), // 6-char hex, enough for max 5 concurrent
            name: opts.name ?? opts.prompt.slice(0, 40),
            prompt: opts.prompt,
            intervalMs: opts.intervalMs,
            createdAtMs: Date.now(),
            tickCount: 0,
            maxTicks: opts.maxTicks ?? DEFAULT_MAX_TICKS
        };
        const timer = setInterval(() => this.tick(loop.id), loop.intervalMs);
        timer.unref();
        this.entries.set(loop.id, { loop, timer });
        this.notifyChange();
        return loop;
    }
    stop(id) {
        const entry = this.entries.get(id);
        if (!entry)
            return false;
        clearInterval(entry.timer);
        this.entries.delete(id);
        this.notifyChange();
        return true;
    }
    stopAll() {
        for (const entry of this.entries.values()) {
            clearInterval(entry.timer);
        }
        this.entries.clear();
        this.notifyChange();
    }
    list() {
        return Array.from(this.entries.values()).map((e) => e.loop);
    }
    get(id) {
        return this.entries.get(id)?.loop;
    }
    get size() {
        return this.entries.size;
    }
    // -------------------------------------------------------------------------
    // Internal
    // -------------------------------------------------------------------------
    /** Returns the loop whose next tick is soonest, or null if no loops active. */
    nextTick() {
        const now = Date.now();
        let best = null;
        for (const { loop } of this.entries.values()) {
            const lastTick = loop.lastTickAtMs ?? loop.createdAtMs;
            const delta = lastTick + loop.intervalMs - now;
            if (!best || delta < best.deltaMs) {
                best = { name: loop.name, deltaMs: delta };
            }
        }
        return best;
    }
    tick(id) {
        const entry = this.entries.get(id);
        if (!entry)
            return;
        const { loop } = entry;
        loop.tickCount++;
        loop.lastTickAtMs = Date.now();
        this.onTick(loop);
        if (loop.tickCount >= loop.maxTicks) {
            clearInterval(entry.timer);
            this.entries.delete(id);
        }
        this.notifyChange();
    }
}
//# sourceMappingURL=loop-manager.js.map