/**
 * Schedule computation — next-run-at logic for all three schedule kinds.
 *
 * Pure functions. No I/O. Caches parsed Cron instances for performance.
 */
import { Cron } from 'croner';
// ---------------------------------------------------------------------------
// Cron expression cache
// ---------------------------------------------------------------------------
const CACHE_MAX = 512;
const cronCache = new Map();
function getCron(expr, tz) {
    const key = tz ? `${expr}|${tz}` : expr;
    let cron = cronCache.get(key);
    if (cron)
        return cron;
    // Evict oldest entry if cache is full (FIFO)
    if (cronCache.size >= CACHE_MAX) {
        const oldest = cronCache.keys().next().value;
        if (oldest)
            cronCache.delete(oldest);
    }
    cron = new Cron(expr, { timezone: tz });
    cronCache.set(key, cron);
    return cron;
}
// ---------------------------------------------------------------------------
// Next-run computation
// ---------------------------------------------------------------------------
/**
 * Compute the next run timestamp (epoch ms) for a schedule.
 * Returns undefined if the schedule has no future runs (e.g. past "at" time).
 */
export function computeNextRunAtMs(schedule, nowMs) {
    switch (schedule.kind) {
        case 'at': {
            const target = new Date(schedule.at).getTime();
            if (!Number.isFinite(target)) {
                throw new Error(`Invalid "at" timestamp: ${schedule.at}`);
            }
            return target > nowMs ? target : undefined;
        }
        case 'every': {
            const everyMs = Math.max(1, Math.floor(schedule.everyMs));
            const anchor = typeof schedule.anchorMs === 'number' && Number.isFinite(schedule.anchorMs)
                ? schedule.anchorMs
                : nowMs;
            if (anchor > nowMs)
                return anchor;
            const elapsed = nowMs - anchor;
            const steps = Math.max(1, Math.ceil(elapsed / everyMs));
            return anchor + steps * everyMs;
        }
        case 'cron': {
            const cron = getCron(schedule.expr, schedule.tz);
            const now = new Date(nowMs);
            let next = cron.nextRun(now);
            // Workaround: croner can return a past time if the reference date
            // is very close to (but just after) a fire time, especially with
            // timezone offsets. Retry with a +1s bump.
            if (next && next.getTime() <= nowMs) {
                const bumped = new Date(nowMs + 1000);
                const retry = cron.nextRun(bumped);
                if (retry && retry.getTime() > nowMs) {
                    next = retry;
                }
            }
            return next ? next.getTime() : undefined;
        }
    }
}
//# sourceMappingURL=schedule.js.map