'use strict';

var events = require('events');
var node_crypto = require('node:crypto');

function createID() {
    return node_crypto.randomUUID();
}

const levelColors = {
    INFO: '\x1b[36m',
    WARN: '\x1b[33m',
    ERROR: '\x1b[31m',
    DEBUG: '\x1b[35m',
};
const GREEN = '\x1b[32m';
const RESET = '\x1b[0m';
function log(level, message, extra) {
    const timestamp = new Date().toISOString();
    const color = levelColors[level];
    const prefix = `[${timestamp}] [PID: ${process.pid}] ${GREEN}[NODE-CRON]${GREEN} ${color}[${level}]${RESET}`;
    const output = `${prefix} ${message}`;
    switch (level) {
        case 'ERROR':
            console.error(output, extra ?? '');
            break;
        case 'DEBUG':
            console.debug(output, extra ?? '');
            break;
        case 'WARN':
            console.warn(output);
            break;
        case 'INFO':
        default:
            console.info(output);
            break;
    }
}
const defaultLogger = {
    info(message) {
        log('INFO', message);
    },
    warn(message) {
        log('WARN', message);
    },
    error(message, err) {
        if (message instanceof Error) {
            log('ERROR', message.message, message);
        }
        else {
            log('ERROR', message, err);
        }
    },
    debug(message, err) {
        if (message instanceof Error) {
            log('DEBUG', message.message, message);
        }
        else {
            log('DEBUG', message, err);
        }
    },
};
const noopLogger = {
    info() { },
    warn() { },
    error() { },
    debug() { },
};
let activeLogger = defaultLogger;
function setLogger(logger) {
    activeLogger = logger ?? defaultLogger;
}
const logger = {
    info: (message) => activeLogger.info(message),
    warn: (message) => activeLogger.warn(message),
    error: (message, err) => activeLogger.error(message, err),
    debug: (message, err) => activeLogger.debug(message, err),
};

class TrackedPromise {
    promise;
    error;
    state;
    value;
    constructor(executor) {
        this.state = 'pending';
        this.promise = new Promise((resolve, reject) => {
            executor((value) => {
                this.state = 'fulfilled';
                this.value = value;
                resolve(value);
            }, (error) => {
                this.state = 'rejected';
                this.error = error;
                reject(error);
            });
        });
    }
    getPromise() {
        return this.promise;
    }
    getState() {
        return this.state;
    }
    isPending() {
        return this.state === 'pending';
    }
    isFulfilled() {
        return this.state === 'fulfilled';
    }
    isRejected() {
        return this.state === 'rejected';
    }
    getValue() {
        return this.value;
    }
    getError() {
        return this.error;
    }
    then(onfulfilled, onrejected) {
        return this.promise.then(onfulfilled, onrejected);
    }
    catch(onrejected) {
        return this.promise.catch(onrejected);
    }
    finally(onfinally) {
        return this.promise.finally(onfinally);
    }
}

function planBeat(expected, now, toleranceMs, getNextMatch) {
    const missed = [];
    let slot = expected;
    while (true) {
        const nowMs = now.getTime();
        const slotMs = slot.getTime();
        if (nowMs < slotMs) {
            return { missed, next: slot };
        }
        const next = getNextMatch(slot);
        if (next.getTime() <= slotMs) {
            return { missed, next: getNextMatch(now) };
        }
        const gap = next.getTime() - slotMs;
        const lateBy = nowMs - slotMs;
        if (lateBy <= toleranceMs && lateBy < gap) {
            return { missed, run: slot, next };
        }
        missed.push(slot);
        slot = next;
    }
}

const DEFAULT_MISSED_EXECUTION_TOLERANCE = 1000;
function emptyOnFn() { }
function emptySkipFn() { }
function emptyHookFn() { return true; }
const DEFAULT_COORDINATOR_TTL = 30000;
class Runner {
    timeMatcher;
    onMatch;
    noOverlap;
    maxExecutions;
    maxRandomDelay;
    missedExecutionTolerance;
    runCount;
    running;
    heartBeatTimeout;
    jitterTimeout;
    logger;
    onMissedExecution;
    onOverlap;
    onError;
    beforeRun;
    onFinished;
    onMaxExecutions;
    runCoordinator;
    coordinatorKeyPrefix;
    coordinatorTtl;
    onSkipped;
    unref;
    constructor(timeMatcher, onMatch, options) {
        this.timeMatcher = timeMatcher;
        this.onMatch = onMatch;
        this.noOverlap = options == undefined || options.noOverlap === undefined ? false : options.noOverlap;
        this.maxExecutions = options?.maxExecutions;
        this.maxRandomDelay = options?.maxRandomDelay || 0;
        this.missedExecutionTolerance = options?.missedExecutionTolerance ?? DEFAULT_MISSED_EXECUTION_TOLERANCE;
        this.logger = options?.logger || logger;
        this.onMissedExecution = options?.onMissedExecution || emptyOnFn;
        this.onOverlap = options?.onOverlap || emptyOnFn;
        this.onError = options?.onError || ((date, error) => this.logger.error('Task failed with error!', error));
        this.onFinished = options?.onFinished || emptyHookFn;
        this.beforeRun = options?.beforeRun || emptyHookFn;
        this.onMaxExecutions = options?.onMaxExecutions || emptyOnFn;
        this.runCoordinator = options?.runCoordinator;
        this.coordinatorKeyPrefix = options?.coordinatorKeyPrefix || '';
        this.coordinatorTtl = options?.coordinatorTtl ?? DEFAULT_COORDINATOR_TTL;
        this.onSkipped = options?.onSkipped || emptySkipFn;
        this.unref = options?.unref ?? false;
        this.runCount = 0;
        this.running = false;
    }
    onErrorFallback = (date, error) => {
        this.logger.error('Task failed with error!', error);
    };
    async runCoordinated(slot, run) {
        if (!this.runCoordinator) {
            await run();
            return;
        }
        const key = `${this.coordinatorKeyPrefix}:${slot.toISOString()}`;
        let allowed;
        try {
            allowed = await this.runCoordinator.shouldRun(key, this.coordinatorTtl);
        }
        catch (err) {
            this.logger.error('Run coordinator failed; skipping execution (fail-closed)', err);
            this.emitSkipped(slot, 'coordinator-error');
            return;
        }
        if (!allowed) {
            this.emitSkipped(slot, 'not-elected');
            return;
        }
        try {
            await run();
        }
        finally {
            try {
                await this.runCoordinator.onComplete?.(key);
            }
            catch (err) {
                this.logger.error('Run coordinator onComplete failed', err);
            }
        }
    }
    emitSkipped(slot, reason) {
        Promise.resolve(this.onSkipped(slot, reason)).catch((err) => this.onErrorFallback(slot, err));
    }
    start() {
        this.running = true;
        let lastExecution;
        let expectedNextExecution = this.timeMatcher.getNextMatch(nowWithoutMs());
        const armHeartBeat = () => {
            if (this.running) {
                clearTimeout(this.heartBeatTimeout);
                this.heartBeatTimeout = setTimeout(heartBeat, getDelay(expectedNextExecution));
                if (this.unref)
                    this.heartBeatTimeout.unref();
            }
        };
        const runTask = async (date) => {
            const execution = {
                id: createID(),
                reason: 'scheduled'
            };
            let shouldExecute;
            try {
                shouldExecute = await this.beforeRun(date, execution);
            }
            catch (error) {
                this.onError(date, error, execution);
                return;
            }
            if (!shouldExecute)
                return;
            const execute = async () => {
                try {
                    this.runCount++;
                    execution.startedAt = new Date();
                    const result = await this.onMatch(date, execution);
                    execution.finishedAt = new Date();
                    execution.result = result;
                }
                catch (error) {
                    execution.finishedAt = new Date();
                    execution.error = error;
                    try {
                        this.onError(date, error, execution);
                    }
                    catch (hookError) {
                        this.onErrorFallback(date, hookError);
                    }
                    return;
                }
                try {
                    await this.onFinished(date, execution);
                }
                catch (hookError) {
                    this.onErrorFallback(date, hookError);
                }
                if (this.maxExecutions && this.runCount >= this.maxExecutions) {
                    this.onMaxExecutions(date);
                    this.stop();
                }
            };
            const randomDelay = Math.floor(Math.random() * this.maxRandomDelay);
            if (randomDelay > 0) {
                await new Promise(resolve => {
                    this.jitterTimeout = setTimeout(() => {
                        execute().then(() => resolve(), () => resolve());
                    }, randomDelay);
                    if (this.unref)
                        this.jitterTimeout.unref();
                });
            }
            else {
                await execute();
            }
        };
        const heartBeat = async () => {
            const currentDate = nowWithoutMs();
            const plan = planBeat(expectedNextExecution, currentDate, this.missedExecutionTolerance, (date) => this.timeMatcher.getNextMatch(date));
            expectedNextExecution = plan.next;
            for (const missedSlot of plan.missed) {
                runAsync(this.onMissedExecution, missedSlot, this.onErrorFallback);
            }
            if (plan.run) {
                if (lastExecution && lastExecution.getState() === 'pending') {
                    runAsync(this.onOverlap, plan.run, this.onErrorFallback);
                    if (this.noOverlap) {
                        this.logger.warn('task still running, new execution blocked by overlap prevention!');
                        armHeartBeat();
                        return;
                    }
                }
                const slot = plan.run;
                lastExecution = new TrackedPromise(async (resolve, reject) => {
                    try {
                        await this.runCoordinated(slot, () => runTask(slot));
                        resolve(true);
                    }
                    catch (err) {
                        reject(err);
                    }
                });
                lastExecution.catch(() => { });
            }
            armHeartBeat();
        };
        armHeartBeat();
    }
    nextRun() {
        return this.timeMatcher.getNextMatch(new Date());
    }
    stop() {
        this.running = false;
        if (this.heartBeatTimeout) {
            clearTimeout(this.heartBeatTimeout);
            this.heartBeatTimeout = undefined;
        }
        if (this.jitterTimeout) {
            clearTimeout(this.jitterTimeout);
            this.jitterTimeout = undefined;
        }
    }
    isStarted() {
        return !!this.heartBeatTimeout && this.running;
    }
    isStopped() {
        return !this.isStarted();
    }
    setUnref(value) {
        this.unref = value;
        if (this.heartBeatTimeout) {
            if (value)
                this.heartBeatTimeout.unref();
            else
                this.heartBeatTimeout.ref();
        }
        if (this.jitterTimeout) {
            if (value)
                this.jitterTimeout.unref();
            else
                this.jitterTimeout.ref();
        }
    }
    async execute(executionId) {
        const date = new Date();
        const execution = {
            id: executionId ?? createID(),
            reason: 'invoked'
        };
        try {
            const shouldExecute = await this.beforeRun(date, execution);
            if (!shouldExecute)
                return;
            execution.startedAt = new Date();
            const result = await this.onMatch(date, execution);
            execution.finishedAt = new Date();
            execution.result = result;
        }
        catch (error) {
            execution.finishedAt = new Date();
            execution.error = error;
            this.onError(date, error, execution);
            return;
        }
        try {
            await this.onFinished(date, execution);
        }
        catch (hookError) {
            this.onErrorFallback(date, hookError);
        }
    }
}
async function runAsync(fn, date, onError) {
    try {
        await fn(date);
    }
    catch (error) {
        onError(date, error);
    }
}
function getDelay(nextRun) {
    const maxDelay = 86400000;
    const now = new Date();
    const delay = nextRun.getTime() - now.getTime();
    if (delay > maxDelay) {
        return maxDelay;
    }
    return Math.max(0, delay);
}
function nowWithoutMs() {
    const date = new Date();
    date.setMilliseconds(0);
    return date;
}

var monthNamesConversion = (() => {
    const months = ['january', 'february', 'march', 'april', 'may', 'june', 'july',
        'august', 'september', 'october', 'november', 'december'];
    const shortMonths = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
        'sep', 'oct', 'nov', 'dec'];
    function convertMonthName(expression, items) {
        for (let i = 0; i < items.length; i++) {
            expression = expression.replace(new RegExp(items[i], 'gi'), i + 1);
        }
        return expression;
    }
    function interpret(monthExpression) {
        monthExpression = convertMonthName(monthExpression, months);
        monthExpression = convertMonthName(monthExpression, shortMonths);
        return monthExpression;
    }
    return interpret;
})();

var weekDayNamesConversion = (() => {
    const weekDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
        'friday', 'saturday'];
    const shortWeekDays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
    function convertWeekDayName(expression, items) {
        for (let i = 0; i < items.length; i++) {
            expression = expression.replace(new RegExp(items[i], 'gi'), i);
        }
        return expression;
    }
    function convertWeekDays(expression) {
        expression = convertWeekDayName(expression, weekDays);
        return convertWeekDayName(expression, shortWeekDays);
    }
    return convertWeekDays;
})();

var convertAsterisksToRanges = (() => {
    function convertAsterisk(expression, replecement) {
        return expression
            .split(',')
            .map((token) => token.indexOf('*') !== -1 ? token.replace('*', replecement) : token)
            .join(',');
    }
    function convertAsterisksToRanges(expressions) {
        expressions[0] = convertAsterisk(expressions[0], '0-59');
        expressions[1] = convertAsterisk(expressions[1], '0-59');
        expressions[2] = convertAsterisk(expressions[2], '0-23');
        expressions[3] = convertAsterisk(expressions[3], '1-31');
        expressions[4] = convertAsterisk(expressions[4], '1-12');
        expressions[5] = convertAsterisk(expressions[5], '0-6');
        return expressions;
    }
    return convertAsterisksToRanges;
})();

var convertRanges = (() => {
    const rangeRegEx = /^(\d+)-(\d+)(?:\/(\d+))?$/;
    const FIELD_BOUNDS = [
        { min: 0, max: 59 },
        { min: 0, max: 59 },
        { min: 0, max: 23 },
        { min: 1, max: 31 },
        { min: 1, max: 12 },
        { min: 0, max: 6 },
    ];
    function expandRange(initTxt, endTxt, stepTxt, bounds) {
        const step = parseInt(stepTxt, 10);
        if (!(step >= 1))
            return `${initTxt}-${endTxt}/${stepTxt}`;
        const first = parseInt(initTxt, 10);
        const last = parseInt(endTxt, 10);
        const numbers = [];
        if (first <= last) {
            for (let i = first; i <= last; i += step) {
                numbers.push(i);
            }
            return numbers.join();
        }
        const { min, max } = bounds;
        const size = max - min + 1;
        const span = ((last - first) % size + size) % size;
        for (let offset = 0; offset <= span; offset += step) {
            let value = first + offset;
            if (value > max)
                value -= size;
            numbers.push(value);
        }
        return numbers.join();
    }
    function convertRange(expression, bounds) {
        return expression
            .split(',')
            .map((token) => {
            const match = rangeRegEx.exec(token.trim());
            return match ? expandRange(match[1], match[2], match[3] || '1', bounds) : token;
        })
            .join();
    }
    function convertAllRanges(expressions) {
        for (let i = 0; i < expressions.length; i++) {
            expressions[i] = convertRange(expressions[i], FIELD_BOUNDS[i]);
        }
        return expressions;
    }
    return convertAllRanges;
})();

const NICKNAMES = {
    '@yearly': '0 0 1 1 *',
    '@annually': '0 0 1 1 *',
    '@monthly': '0 0 1 * *',
    '@weekly': '0 0 * * 0',
    '@daily': '0 0 * * *',
    '@midnight': '0 0 * * *',
    '@hourly': '0 * * * *',
};
function resolveNickname(expression) {
    const key = expression.trim().toLowerCase();
    return NICKNAMES[key] ?? expression;
}

var convertExpression = (() => {
    function appendSecondExpression(expressions) {
        if (expressions.length === 5) {
            return ['0'].concat(expressions);
        }
        return expressions;
    }
    function removeSpaces(str) {
        return str.replace(/\s{2,}/g, ' ').trim();
    }
    function normalizeIntegers(expressions) {
        for (let i = 0; i < expressions.length; i++) {
            const numbers = expressions[i].split(',');
            for (let j = 0; j < numbers.length; j++) {
                const token = String(numbers[j]).trim();
                if (/^l$/i.test(token)) {
                    numbers[j] = 'L';
                }
                else if (/^l-\d{1,2}$/i.test(token)) {
                    numbers[j] = token.toUpperCase();
                }
                else if (/^[0-7]l$/i.test(token)) {
                    numbers[j] = token.toUpperCase();
                }
                else if (/w/i.test(token)) {
                    numbers[j] = token.toUpperCase();
                }
                else if (token.indexOf('#') !== -1) {
                    numbers[j] = token;
                }
                else if (/^\d+$/.test(token)) {
                    numbers[j] = parseInt(token, 10);
                }
                else {
                    numbers[j] = token;
                }
            }
            expressions[i] = numbers;
        }
        return expressions;
    }
    function convertQuestionMarks(expressions) {
        if (expressions[3] === '?')
            expressions[3] = '*';
        if (expressions[5] === '?')
            expressions[5] = '*';
        return expressions;
    }
    function interpret(expression) {
        let expressions = removeSpaces(resolveNickname(`${expression}`)).split(' ');
        expressions = appendSecondExpression(expressions);
        expressions = convertQuestionMarks(expressions);
        expressions[4] = monthNamesConversion(expressions[4]);
        expressions[5] = weekDayNamesConversion(expressions[5]);
        expressions = convertAsterisksToRanges(expressions);
        expressions = convertRanges(expressions);
        expressions = normalizeIntegers(expressions);
        const weekdays = expressions[5];
        for (let i = 0; i < weekdays.length; i++) {
            if (weekdays[i] === 7)
                weekdays[i] = 0;
            else if (typeof weekdays[i] === 'string' && weekdays[i].startsWith('7')) {
                weekdays[i] = '0' + weekdays[i].slice(1);
            }
        }
        expressions[5] = [...new Set(weekdays)];
        return expressions;
    }
    return interpret;
})();

class LocalizedTime {
    timestamp;
    parts;
    timezone;
    constructor(date, timezone) {
        this.timestamp = date.getTime();
        this.timezone = timezone;
        this.parts = buildDateParts(date, timezone);
    }
    toDate() {
        return new Date(this.timestamp);
    }
    toISO() {
        const gmt = this.parts.gmt.replace(/^GMT/, '');
        const offset = gmt ? gmt : 'Z';
        const pad = (n) => String(n).padStart(2, '0');
        return `${this.parts.year}-${pad(this.parts.month)}-${pad(this.parts.day)}`
            + `T${pad(this.parts.hour)}:${pad(this.parts.minute)}:${pad(this.parts.second)}`
            + `.${String(this.parts.millisecond).padStart(3, '0')}`
            + offset;
    }
    getParts() {
        return this.parts;
    }
}
function getOffsetMinutes(date, timezone) {
    const offset = parseOffsetMinutes(getTimezoneGMT(date, timezone).replace(/^GMT/, '') || 'Z');
    return offset ?? 0;
}
function readsBackTo(timestamp, parts, timezone) {
    const p = buildDateParts(new Date(timestamp), timezone);
    return p.year === parts.year && p.month === parts.month && p.day === parts.day
        && p.hour === parts.hour && p.minute === parts.minute && p.second === parts.second;
}
function localTimeToTimestamp(parts, timezone) {
    const guess = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second, parts.millisecond);
    const firstOffset = getOffsetMinutes(new Date(guess), timezone);
    const candidate1 = guess - firstOffset * 60000;
    const secondOffset = getOffsetMinutes(new Date(candidate1), timezone);
    if (secondOffset === firstOffset) {
        return candidate1;
    }
    const candidate2 = guess - secondOffset * 60000;
    if (readsBackTo(candidate1, parts, timezone))
        return candidate1;
    if (readsBackTo(candidate2, parts, timezone))
        return candidate2;
    return Math.max(candidate1, candidate2);
}
const partsFormatterCache = new Map();
const offsetFormatterCache = new Map();
function getPartsFormatter(timezone) {
    const key = timezone ?? '';
    let formatter = partsFormatterCache.get(key);
    if (!formatter) {
        const dftOptions = {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            weekday: 'short',
            hour12: false
        };
        if (timezone) {
            dftOptions.timeZone = timezone;
        }
        formatter = new Intl.DateTimeFormat('en-US', dftOptions);
        partsFormatterCache.set(key, formatter);
    }
    return formatter;
}
function getOffsetFormatter(timezone) {
    const key = timezone ?? '';
    let formatter = offsetFormatterCache.get(key);
    if (!formatter) {
        formatter = new Intl.DateTimeFormat('en-US', {
            timeZone: timezone,
            timeZoneName: 'shortOffset'
        });
        offsetFormatterCache.set(key, formatter);
    }
    return formatter;
}
function buildDateParts(date, timezone) {
    const dateFormat = getPartsFormatter(timezone);
    const parts = dateFormat.formatToParts(date).filter(part => {
        return part.type !== 'literal';
    }).reduce((acc, part) => {
        acc[part.type] = part.value;
        return acc;
    }, {});
    const result = {
        day: parseInt(parts.day),
        month: parseInt(parts.month),
        year: parseInt(parts.year),
        hour: parts.hour === '24' ? 0 : parseInt(parts.hour),
        minute: parseInt(parts.minute),
        second: parseInt(parts.second),
        millisecond: date.getMilliseconds(),
        weekday: parts.weekday
    };
    let gmt;
    Object.defineProperty(result, 'gmt', {
        enumerable: true,
        configurable: true,
        get() {
            return gmt ??= getTimezoneGMT(date, timezone);
        }
    });
    return result;
}
function parseOffsetMinutes(isoString) {
    if (isoString.endsWith('Z'))
        return 0;
    const match = isoString.match(/([+-])(\d{2}):(\d{2})$/);
    if (!match)
        return null;
    const sign = match[1] === '+' ? 1 : -1;
    return sign * (parseInt(match[2]) * 60 + parseInt(match[3]));
}
function getTimezoneGMT(date, timezone) {
    const fmt = getOffsetFormatter(timezone);
    const parts = fmt.formatToParts(date);
    const tzPart = parts.find(p => p.type === 'timeZoneName');
    if (!tzPart)
        return 'Z';
    const tzValue = tzPart.value;
    if (tzValue === 'GMT')
        return 'Z';
    const match = tzValue.match(/^GMT([+-])(\d{1,2})(?::(\d{2}))?$/);
    if (!match)
        return 'Z';
    const sign = match[1];
    const hoursNum = parseInt(match[2]);
    const minutesNum = parseInt(match[3] || '0');
    if (hoursNum === 0 && minutesNum === 0)
        return 'Z';
    const hours = match[2].padStart(2, '0');
    const minutes = (match[3] || '00').padStart(2, '0');
    return `GMT${sign}${hours}:${minutes}`;
}

const LAST_DAY_TOKEN = 'L';
const WEEKDAY_TOKEN = /^(\d{1,2}|L)W$/;
const LAST_DAY_OFFSET_TOKEN = /^L-(\d{1,2})$/;
function lastDayOfMonth(year, month) {
    return new Date(Date.UTC(year, month, 0)).getUTCDate();
}
function nearestWeekday(year, month, target) {
    const last = lastDayOfMonth(year, month);
    if (target < 1 || target > last)
        return -1;
    const weekday = new Date(Date.UTC(year, month - 1, target)).getUTCDay();
    if (weekday === 6)
        return target === 1 ? target + 2 : target - 1;
    if (weekday === 0)
        return target === last ? target - 2 : target + 1;
    return target;
}
function matchesDayOfMonth(field, year, month, day) {
    for (const value of field) {
        if (value === day)
            return true;
        if (value === LAST_DAY_TOKEN && day === lastDayOfMonth(year, month))
            return true;
        if (typeof value === 'string') {
            const weekdayMatch = WEEKDAY_TOKEN.exec(value);
            if (weekdayMatch) {
                const target = weekdayMatch[1] === LAST_DAY_TOKEN ? lastDayOfMonth(year, month) : parseInt(weekdayMatch[1], 10);
                if (nearestWeekday(year, month, target) === day)
                    return true;
            }
            const offsetMatch = LAST_DAY_OFFSET_TOKEN.exec(value);
            if (offsetMatch) {
                const target = lastDayOfMonth(year, month) - parseInt(offsetMatch[1], 10);
                if (target >= 1 && target === day)
                    return true;
            }
        }
    }
    return false;
}

const LAST_WEEKDAY_REGEX = /^([0-7])L$/i;
const NTH_WEEKDAY_REGEX = /^([0-7])#([1-5])$/;
function parseLastWeekdayToken(value) {
    if (typeof value !== 'string')
        return null;
    const match = LAST_WEEKDAY_REGEX.exec(value);
    if (!match)
        return null;
    const weekday = parseInt(match[1], 10);
    return weekday === 7 ? 0 : weekday;
}
function isLastWeekdayOfMonth(year, month, day) {
    const date = new Date(Date.UTC(year, month - 1, day));
    const inSevenDays = new Date(date.getTime());
    inSevenDays.setUTCDate(inSevenDays.getUTCDate() + 7);
    return inSevenDays.getUTCMonth() + 1 !== month;
}
function isNthWeekdayToken(value) {
    return typeof value === 'string' && NTH_WEEKDAY_REGEX.test(value);
}
function parseNthWeekday(value) {
    if (typeof value !== 'string')
        return null;
    const match = NTH_WEEKDAY_REGEX.exec(value);
    if (!match)
        return null;
    const weekday = parseInt(match[1], 10) % 7;
    const nth = parseInt(match[2], 10);
    return { weekday, nth };
}
function occurrenceInMonth(day) {
    return Math.floor((day - 1) / 7) + 1;
}
function matchesNthWeekday(token, year, month, day) {
    const parsed = parseNthWeekday(token);
    if (!parsed)
        return false;
    const weekday = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
    if (weekday !== parsed.weekday)
        return false;
    return occurrenceInMonth(day) === parsed.nth;
}
function matchesDayOfWeek(field, year, month, day, weekday) {
    for (const value of field) {
        if (value === weekday)
            return true;
        if (isNthWeekdayToken(value)) {
            if (matchesNthWeekday(value, year, month, day))
                return true;
            continue;
        }
        const lastWeekday = parseLastWeekdayToken(value);
        if (lastWeekday !== null &&
            lastWeekday === weekday &&
            isLastWeekdayOfMonth(year, month, day)) {
            return true;
        }
    }
    return false;
}

const MAX_DAYS = 366 * 100;
class MatcherWalker {
    baseDate;
    timeMatcher;
    timezone;
    seconds;
    minutes;
    hours;
    days;
    months;
    weekdays;
    constructor(timeMatcher, baseDate, timezone) {
        this.baseDate = baseDate;
        this.timeMatcher = timeMatcher;
        this.timezone = timezone;
        const expressions = timeMatcher.expressions;
        this.seconds = sortedAsc(expressions[0]);
        this.minutes = sortedAsc(expressions[1]);
        this.hours = sortedAsc(expressions[2]);
        this.days = expressions[3];
        this.months = expressions[4];
        this.weekdays = expressions[5];
    }
    isMatching() {
        return this.timeMatcher.match(this.baseDate);
    }
    matchNext() {
        const months = this.months;
        const days = this.days;
        const baseMs = Math.floor(this.baseDate.getTime() / 1000) * 1000;
        const baseParts = new LocalizedTime(new Date(baseMs), this.timezone).getParts();
        let { year, month, day } = baseParts;
        for (let i = 0; i < MAX_DAYS; i++) {
            if (months.includes(month) && matchesDayOfMonth(days, year, month, day) && this.matchesWeekday(year, month, day)) {
                const lowerBound = i === 0 ? baseParts : null;
                const found = this.firstTimeOnDay(year, month, day, lowerBound, baseMs);
                if (found !== null) {
                    return new LocalizedTime(new Date(found), this.timezone);
                }
            }
            ({ year, month, day } = nextDay(year, month, day));
        }
        throw new Error('Could not find next matching date within reasonable time range');
    }
    firstTimeOnDay(year, month, day, lowerBound, baseMs) {
        const { seconds, minutes, hours } = this;
        for (const hour of hours) {
            if (lowerBound && hour < lowerBound.hour)
                continue;
            for (const minute of minutes) {
                for (const second of seconds) {
                    if (lowerBound && !isLaterInDay(hour, minute, second, lowerBound))
                        continue;
                    const ts = localTimeToTimestamp({ year, month, day, hour, minute, second, millisecond: 0 }, this.timezone);
                    if (ts > baseMs && this.timeMatcher.match(new Date(ts))) {
                        return ts;
                    }
                }
            }
        }
        return null;
    }
    matchesWeekday(year, month, day) {
        const weekday = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
        return matchesDayOfWeek(this.weekdays, year, month, day, weekday);
    }
}
function nextDay(year, month, day) {
    const d = new Date(Date.UTC(year, month - 1, day + 1));
    return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
}
function sortedAsc(values) {
    return [...values].sort((a, b) => a - b);
}
function isLaterInDay(hour, minute, second, bound) {
    return hour * 3600 + minute * 60 + second > bound.hour * 3600 + bound.minute * 60 + bound.second;
}

function matchValue(allowedValues, value) {
    return allowedValues.indexOf(value) !== -1;
}
class TimeMatcher {
    timezone;
    pattern;
    expressions;
    constructor(pattern, timezone) {
        this.timezone = timezone;
        this.pattern = pattern;
        this.expressions = convertExpression(pattern);
    }
    match(date) {
        const localizedTime = new LocalizedTime(date, this.timezone);
        const parts = localizedTime.getParts();
        const runOnSecond = matchValue(this.expressions[0], parts.second);
        const runOnMinute = matchValue(this.expressions[1], parts.minute);
        const runOnHour = matchValue(this.expressions[2], parts.hour);
        const runOnDay = matchesDayOfMonth(this.expressions[3], parts.year, parts.month, parts.day);
        const runOnMonth = matchValue(this.expressions[4], parts.month);
        const weekday = parseInt(weekDayNamesConversion(parts.weekday));
        const runOnWeekDay = matchesDayOfWeek(this.expressions[5], parts.year, parts.month, parts.day, weekday);
        return runOnSecond && runOnMinute && runOnHour && runOnDay && runOnMonth && runOnWeekDay;
    }
    getNextMatch(date) {
        const walker = new MatcherWalker(this, date, this.timezone);
        const next = walker.matchNext();
        return next.toDate();
    }
}

const allowedTransitions = {
    'stopped': ['stopped', 'idle', 'destroyed'],
    'idle': ['idle', 'running', 'stopped', 'destroyed'],
    'running': ['running', 'idle', 'stopped', 'destroyed'],
    'destroyed': ['destroyed']
};
class StateMachine {
    state;
    constructor(initial = 'stopped') {
        this.state = initial;
    }
    changeState(state) {
        if (allowedTransitions[this.state].includes(state)) {
            this.state = state;
        }
        else {
            throw new Error(`invalid transition from ${this.state} to ${state}`);
        }
    }
}

class EnvVarRunCoordinator {
    envName;
    constructor(envName = 'NODE_CRON_RUN') {
        this.envName = envName;
        this.read();
    }
    shouldRun() {
        return this.read();
    }
    read() {
        const value = process.env[this.envName];
        if (value !== 'true' && value !== 'false') {
            throw new Error(`node-cron: a \`distributed\` task needs ${this.envName} set to 'true' or 'false'. ` +
                `Set it to 'true' on exactly one instance and 'false' on the others, ` +
                `or provide a coordinator via cron.setRunCoordinator(...).`);
        }
        return value === 'true';
    }
}

let globalRunCoordinator;
function setRunCoordinator(coordinator) {
    globalRunCoordinator = coordinator;
}
function resolveRunCoordinator(perTask) {
    return perTask ?? globalRunCoordinator ?? new EnvVarRunCoordinator();
}

class TaskEmitter extends events.EventEmitter {
}
function safeEmit(emitter, event, context, onError) {
    for (const listener of emitter.listeners(event)) {
        try {
            Promise.resolve(listener(context)).catch(onError);
        }
        catch (error) {
            onError(error);
        }
    }
}
class InlineScheduledTask {
    emitter;
    cronExpression;
    timeMatcher;
    runner;
    id;
    name;
    stateMachine;
    timezone;
    logger;
    suppressMissedWarning;
    _lastRun = null;
    constructor(cronExpression, taskFn, options) {
        this.emitter = new TaskEmitter();
        this.cronExpression = cronExpression;
        this.id = createID();
        this.name = options?.name || this.id;
        this.timezone = options?.timezone;
        this.logger = options?.logger || logger;
        this.suppressMissedWarning = options?.suppressMissedWarning || false;
        this.timeMatcher = new TimeMatcher(cronExpression, options?.timezone);
        this.stateMachine = new StateMachine();
        const runnerOptions = {
            timezone: options?.timezone,
            noOverlap: options?.noOverlap,
            maxExecutions: options?.maxExecutions,
            maxRandomDelay: options?.maxRandomDelay,
            missedExecutionTolerance: options?.missedExecutionTolerance,
            logger: this.logger,
            beforeRun: (date, execution) => {
                if (execution.reason === 'scheduled') {
                    this.changeState('running');
                }
                this.emit('execution:started', this.createContext(date, execution));
                return true;
            },
            onFinished: (date, execution) => {
                if (execution.reason === 'scheduled') {
                    this.changeState('idle');
                }
                this.recordLastRun(execution);
                this.emit('execution:finished', this.createContext(date, execution));
                return true;
            },
            onError: (date, error, execution) => {
                this.logger.error(error);
                this.recordLastRun(execution);
                this.emit('execution:failed', this.createContext(date, execution));
                this.changeState('idle');
            },
            onOverlap: (date) => {
                this.emit('execution:overlap', this.createContext(date));
            },
            onMissedExecution: (date) => {
                const handled = this.emitter.listenerCount('execution:missed') > 0;
                if (!this.suppressMissedWarning && !handled) {
                    this.logger.warn(`missed execution at ${date}! Possible blocking IO or high CPU user at the same process used by node-cron.`);
                }
                this.emit('execution:missed', this.createContext(date));
            },
            onMaxExecutions: (date) => {
                this.emit('execution:maxReached', this.createContext(date));
                this.destroy();
            },
            runCoordinator: options?.distributed ? resolveRunCoordinator(options?.runCoordinator) : undefined,
            coordinatorKeyPrefix: this.name,
            coordinatorTtl: options?.distributedLease,
            onSkipped: (date, reason) => {
                this.emit('execution:skipped', this.createContext(date, undefined, reason));
            },
            unref: options?.unref
        };
        this.runner = new Runner(this.timeMatcher, (date, execution) => {
            return taskFn(this.createContext(date, execution));
        }, runnerOptions);
    }
    getNextRun() {
        if (this.stateMachine.state !== 'stopped') {
            return this.runner.nextRun();
        }
        return null;
    }
    getNextRuns(count) {
        const runs = [];
        let from = new Date();
        for (let i = 0; i < count; i++) {
            from = this.timeMatcher.getNextMatch(from);
            runs.push(from);
        }
        return runs;
    }
    match(date) {
        return this.timeMatcher.match(date);
    }
    msToNext() {
        const next = this.getNextRun();
        return next ? next.getTime() - Date.now() : null;
    }
    isBusy() {
        return this.getStatus() === 'running';
    }
    runsLeft() {
        if (this.runner.maxExecutions == null)
            return undefined;
        return Math.max(0, this.runner.maxExecutions - this.runner.runCount);
    }
    getPattern() {
        return this.cronExpression;
    }
    lastRun() {
        return this._lastRun;
    }
    recordLastRun(execution) {
        const date = execution.finishedAt;
        const lastRun = { date };
        if (execution.error) {
            lastRun.error = execution.error;
        }
        else {
            lastRun.result = execution.result;
        }
        this._lastRun = lastRun;
    }
    emit(event, context) {
        safeEmit(this.emitter, event, context, (error) => this.logger.error(error));
    }
    changeState(state) {
        if (this.runner.isStarted()) {
            this.stateMachine.changeState(state);
        }
    }
    start() {
        if (this.stateMachine.state === 'destroyed')
            return;
        if (this.runner.isStopped()) {
            this.runner.start();
            this.stateMachine.changeState('idle');
            this.emit('task:started', this.createContext(new Date()));
        }
    }
    stop() {
        if (this.runner.isStarted()) {
            this.runner.stop();
            this.stateMachine.changeState('stopped');
            this.emit('task:stopped', this.createContext(new Date()));
        }
    }
    getStatus() {
        return this.stateMachine.state;
    }
    unref() {
        this.runner.setUnref(true);
    }
    ref() {
        this.runner.setUnref(false);
    }
    destroy() {
        if (this.stateMachine.state === 'destroyed')
            return;
        this.stop();
        this.stateMachine.changeState('destroyed');
        this.emit('task:destroyed', this.createContext(new Date()));
    }
    execute(executionId) {
        const id = executionId ?? createID();
        return new Promise((resolve, reject) => {
            const onFail = (context) => {
                if (context.execution?.id !== id)
                    return;
                this.off('execution:finished', onFinished);
                this.off('execution:failed', onFail);
                reject(context.execution?.error);
            };
            const onFinished = (context) => {
                if (context.execution?.id !== id)
                    return;
                this.off('execution:finished', onFinished);
                this.off('execution:failed', onFail);
                resolve(context.execution?.result);
            };
            this.on('execution:finished', onFinished);
            this.on('execution:failed', onFail);
            this.runner.execute(id);
        });
    }
    on(event, fun) {
        this.emitter.on(event, fun);
    }
    off(event, fun) {
        this.emitter.off(event, fun);
    }
    once(event, fun) {
        this.emitter.once(event, fun);
    }
    createContext(executionDate, execution, reason) {
        const localTime = new LocalizedTime(executionDate, this.timezone);
        const ctx = {
            date: localTime.toDate(),
            dateLocalIso: localTime.toISO(),
            triggeredAt: new Date(),
            task: this,
            execution: execution
        };
        if (reason)
            ctx.reason = reason;
        return ctx;
    }
}

exports.InlineScheduledTask = InlineScheduledTask;
exports.LocalizedTime = LocalizedTime;
exports.StateMachine = StateMachine;
exports.TimeMatcher = TimeMatcher;
exports.convertExpression = convertExpression;
exports.createID = createID;
exports.isNthWeekdayToken = isNthWeekdayToken;
exports.logger = logger;
exports.noopLogger = noopLogger;
exports.resolveNickname = resolveNickname;
exports.resolveRunCoordinator = resolveRunCoordinator;
exports.setLogger = setLogger;
exports.setRunCoordinator = setRunCoordinator;
//# sourceMappingURL=_shared.cjs.map
