// ============================================================================
// Constants
// ============================================================================
const MONTH_NAMES = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
];
const DAY_NAMES = [
    "Sunday",
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
];
const DAY_SHORT_NAMES = [
    "Sun",
    "Mon",
    "Tue",
    "Wed",
    "Thu",
    "Fri",
    "Sat",
];
/**
 * Preset cron schedules for common use cases
 */
const CRON_PRESETS = [
    {
        label: "Every minute",
        value: "* * * * *",
        description: "Runs every minute",
    },
    {
        label: "Every hour",
        value: "0 * * * *",
        description: "At the start of every hour",
    },
    {
        label: "Every day at midnight",
        value: "0 0 * * *",
        description: "Once a day at 00:00",
    },
    {
        label: "Every Monday",
        value: "0 9 * * 1",
        description: "At 09:00 on Monday",
    },
    {
        label: "Every weekday",
        value: "0 9 * * 1-5",
        description: "At 09:00, Monday through Friday",
    },
    {
        label: "Monthly",
        value: "0 0 1 * *",
        description: "At midnight on the 1st of every month",
    },
    {
        label: "Yearly",
        value: "0 0 1 1 *",
        description: "At midnight on January 1st",
    },
];
/**
 * Generate minute options: every minute (*), specific minutes, and intervals
 */
function getMinuteOptions() {
    const options = [
        { value: "*", label: "Every minute" },
        { value: "*/5", label: "Every 5 minutes" },
        { value: "*/10", label: "Every 10 minutes" },
        { value: "*/15", label: "Every 15 minutes" },
        { value: "*/30", label: "Every 30 minutes" },
    ];
    for (let i = 0; i < 60; i++) {
        options.push({
            value: String(i),
            label: String(i).padStart(2, "0"),
        });
    }
    return options;
}
/**
 * Generate hour options: every hour (*), specific hours, and intervals
 */
function getHourOptions() {
    const options = [
        { value: "*", label: "Every hour" },
        { value: "*/2", label: "Every 2 hours" },
        { value: "*/4", label: "Every 4 hours" },
        { value: "*/6", label: "Every 6 hours" },
        { value: "*/12", label: "Every 12 hours" },
    ];
    for (let i = 0; i < 24; i++) {
        const hour12 = i === 0 ? 12 : i > 12 ? i - 12 : i;
        const ampm = i < 12 ? "AM" : "PM";
        options.push({
            value: String(i),
            label: `${String(i).padStart(2, "0")}:00 (${hour12}${ampm})`,
        });
    }
    return options;
}
/**
 * Generate day-of-month options: every day (*), specific days
 */
function getDayOfMonthOptions() {
    const options = [
        { value: "*", label: "Every day" },
    ];
    for (let i = 1; i <= 31; i++) {
        const suffix = getOrdinalSuffix(i);
        options.push({
            value: String(i),
            label: `${i}${suffix}`,
        });
    }
    return options;
}
/**
 * Generate month options: every month (*), specific months
 */
function getMonthOptions() {
    const options = [
        { value: "*", label: "Every month" },
    ];
    for (let i = 1; i <= 12; i++) {
        options.push({
            value: String(i),
            label: MONTH_NAMES[i - 1],
        });
    }
    return options;
}
/**
 * Generate day-of-week options: every day (*), specific days, ranges
 */
function getDayOfWeekOptions() {
    const options = [
        { value: "*", label: "Every day" },
        { value: "1-5", label: "Weekdays (Mon-Fri)" },
        { value: "0,6", label: "Weekends (Sat-Sun)" },
    ];
    for (let i = 0; i <= 6; i++) {
        options.push({
            value: String(i),
            label: DAY_NAMES[i],
        });
    }
    return options;
}
// ============================================================================
// Parsing
// ============================================================================
/**
 * Parse a cron expression string into individual fields
 */
function parseCron(expr) {
    const trimmed = expr.trim();
    const parts = trimmed.split(/\s+/);
    if (parts.length !== 5) {
        return {
            minute: "*",
            hour: "*",
            dayOfMonth: "*",
            month: "*",
            dayOfWeek: "*",
        };
    }
    return {
        minute: parts[0],
        hour: parts[1],
        dayOfMonth: parts[2],
        month: parts[3],
        dayOfWeek: parts[4],
    };
}
/**
 * Convert CronFields back to a cron expression string
 */
function fieldsToCron(fields) {
    return `${fields.minute} ${fields.hour} ${fields.dayOfMonth} ${fields.month} ${fields.dayOfWeek}`;
}
// ============================================================================
// Validation
// ============================================================================
/**
 * Validate whether a single cron field value is valid
 */
function isValidCronField(value, min, max) {
    if (value === "*")
        return true;
    // Step values: */5, 1-30/5
    if (value.includes("/")) {
        const [range, stepStr] = value.split("/");
        const step = parseInt(stepStr, 10);
        if (isNaN(step) || step < 1)
            return false;
        if (range === "*")
            return step <= max;
        return isValidCronField(range, min, max);
    }
    // Ranges: 1-5
    if (value.includes("-")) {
        const [startStr, endStr] = value.split("-");
        const start = parseInt(startStr, 10);
        const end = parseInt(endStr, 10);
        return (!isNaN(start) &&
            !isNaN(end) &&
            start >= min &&
            end <= max &&
            start <= end);
    }
    // Lists: 1,3,5
    if (value.includes(",")) {
        return value.split(",").every((v) => isValidCronField(v.trim(), min, max));
    }
    // Single number
    const num = parseInt(value, 10);
    return !isNaN(num) && num >= min && num <= max;
}
/**
 * Validate a complete cron expression
 */
function validateCron(expr) {
    const trimmed = expr.trim();
    const parts = trimmed.split(/\s+/);
    if (parts.length !== 5)
        return false;
    const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
    return (isValidCronField(minute, 0, 59) &&
        isValidCronField(hour, 0, 23) &&
        isValidCronField(dayOfMonth, 1, 31) &&
        isValidCronField(month, 1, 12) &&
        isValidCronField(dayOfWeek, 0, 6));
}
// ============================================================================
// Human-readable description
// ============================================================================
/**
 * Get ordinal suffix for a number (1st, 2nd, 3rd, etc.)
 */
function getOrdinalSuffix(n) {
    const s = ["th", "st", "nd", "rd"];
    const v = n % 100;
    return s[(v - 20) % 10] || s[v] || s[0];
}
/**
 * Describe a minute field
 */
function describeMinute(minute) {
    if (minute === "*")
        return "";
    if (minute.startsWith("*/"))
        return `every ${minute.slice(2)} minutes`;
    return `at minute ${minute}`;
}
/**
 * Describe an hour field
 */
function describeHour(hour) {
    if (hour === "*")
        return "";
    if (hour.startsWith("*/"))
        return `every ${hour.slice(2)} hours`;
    const h = parseInt(hour, 10);
    if (isNaN(h))
        return "";
    const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
    const ampm = h < 12 ? "AM" : "PM";
    return `${hour12}:00 ${ampm}`;
}
/**
 * Describe the time portion (minute + hour)
 */
function describeTime(minute, hour) {
    if (minute === "*" && hour === "*")
        return "every minute";
    if (minute === "*" && hour !== "*") {
        if (hour.startsWith("*/"))
            return `every ${hour.slice(2)} hours`;
        return `every minute during ${describeHour(hour)}`;
    }
    if (minute !== "*" && hour === "*") {
        if (minute.startsWith("*/"))
            return `${describeMinute(minute)}`;
        return `at minute ${minute} of every hour`;
    }
    // Both specific
    const h = parseInt(hour, 10);
    const m = parseInt(minute, 10);
    if (!isNaN(h) && !isNaN(m)) {
        const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
        const ampm = h < 12 ? "AM" : "PM";
        return `at ${String(hour12).padStart(2, "0")}:${String(m).padStart(2, "0")} ${ampm}`;
    }
    // Fallback for intervals
    const parts = [];
    if (minute.startsWith("*/"))
        parts.push(describeMinute(minute));
    if (hour.startsWith("*/"))
        parts.push(describeHour(hour));
    if (parts.length > 0)
        return parts.join(", ");
    return `at ${hour}:${minute}`;
}
/**
 * Convert a cron expression to a human-readable description
 */
function cronToDescription(expr) {
    if (!validateCron(expr))
        return "Invalid cron expression";
    const { minute, hour, dayOfMonth, month, dayOfWeek } = parseCron(expr);
    const timePart = describeTime(minute, hour);
    const parts = [];
    // Time
    if (timePart) {
        // Capitalize first letter
        parts.push(timePart.charAt(0).toUpperCase() + timePart.slice(1));
    }
    // Day of week
    if (dayOfWeek !== "*") {
        if (dayOfWeek === "1-5") {
            parts.push("on weekdays");
        }
        else if (dayOfWeek === "0,6") {
            parts.push("on weekends");
        }
        else if (dayOfWeek.includes(",")) {
            const days = dayOfWeek
                .split(",")
                .map((d) => DAY_NAMES[parseInt(d, 10)] || d);
            parts.push(`on ${days.join(" and ")}`);
        }
        else if (dayOfWeek.includes("-")) {
            const [start, end] = dayOfWeek.split("-").map((d) => parseInt(d, 10));
            parts.push(`on ${DAY_SHORT_NAMES[start]} through ${DAY_SHORT_NAMES[end]}`);
        }
        else {
            const dayIdx = parseInt(dayOfWeek, 10);
            if (!isNaN(dayIdx) && dayIdx >= 0 && dayIdx <= 6) {
                parts.push(`on ${DAY_NAMES[dayIdx]}`);
            }
        }
    }
    // Day of month
    if (dayOfMonth !== "*") {
        const day = parseInt(dayOfMonth, 10);
        if (!isNaN(day)) {
            parts.push(`on the ${day}${getOrdinalSuffix(day)}`);
        }
    }
    // Month
    if (month !== "*") {
        const monthIdx = parseInt(month, 10);
        if (!isNaN(monthIdx) && monthIdx >= 1 && monthIdx <= 12) {
            parts.push(`in ${MONTH_NAMES[monthIdx - 1]}`);
        }
    }
    if (parts.length === 0)
        return "Every minute";
    return parts.join(" ");
}
// ============================================================================
// Next run calculation
// ============================================================================
/**
 * Expand a cron field value into an array of matching numbers.
 * Handles *, specific values, ranges, lists, and step values.
 */
function expandField(value, min, max) {
    if (value === "*") {
        return Array.from({ length: max - min + 1 }, (_, i) => min + i);
    }
    // Step values: */5 or 1-30/5
    if (value.includes("/")) {
        const [range, stepStr] = value.split("/");
        const step = parseInt(stepStr, 10);
        const result = [];
        let rangeMin = min;
        let rangeMax = max;
        if (range !== "*") {
            if (range.includes("-")) {
                const [rMin, rMax] = range.split("-").map(Number);
                rangeMin = rMin;
                rangeMax = rMax;
            }
            else {
                rangeMin = parseInt(range, 10);
            }
        }
        for (let i = rangeMin; i <= rangeMax; i += step) {
            result.push(i);
        }
        return result;
    }
    // Lists: 1,3,5
    if (value.includes(",")) {
        return value.split(",").map((v) => parseInt(v.trim(), 10));
    }
    // Ranges: 1-5
    if (value.includes("-")) {
        const [start, end] = value.split("-").map(Number);
        return Array.from({ length: end - start + 1 }, (_, i) => start + i);
    }
    // Single number
    return [parseInt(value, 10)];
}
/**
 * Check if a given Date matches a cron expression
 */
function matchesCron(date, fields) {
    const minutes = expandField(fields.minute, 0, 59);
    const hours = expandField(fields.hour, 0, 23);
    const daysOfMonth = expandField(fields.dayOfMonth, 1, 31);
    const months = expandField(fields.month, 1, 12);
    const daysOfWeek = expandField(fields.dayOfWeek, 0, 6);
    return (minutes.includes(date.getMinutes()) &&
        hours.includes(date.getHours()) &&
        daysOfMonth.includes(date.getDate()) &&
        months.includes(date.getMonth() + 1) &&
        daysOfWeek.includes(date.getDay()));
}
/**
 * Calculate the next N run times for a cron expression.
 * Uses a brute-force minute-by-minute scan (simple approach for common cases).
 *
 * @param expr - Cron expression string
 * @param count - Number of next runs to find (default: 3)
 * @param from - Starting date (default: now)
 * @returns Array of Date objects for the next runs
 */
function getNextRuns(expr, count = 3, from) {
    if (!validateCron(expr))
        return [];
    const fields = parseCron(expr);
    const results = [];
    // Start from the next minute
    const start = from ? new Date(from) : new Date();
    start.setSeconds(0, 0);
    start.setMinutes(start.getMinutes() + 1);
    const current = new Date(start);
    // Scan up to 2 years ahead, max 525960 iterations (minutes in a year * 2)
    // to avoid infinite loops
    const maxIterations = 525960 * 2;
    let iterations = 0;
    while (results.length < count && iterations < maxIterations) {
        if (matchesCron(current, fields)) {
            results.push(new Date(current));
        }
        current.setMinutes(current.getMinutes() + 1);
        iterations++;
    }
    return results;
}
/**
 * Format a Date as a human-readable next run string
 */
function formatNextRun(date) {
    const now = new Date();
    const diffMs = date.getTime() - now.getTime();
    const diffMinutes = Math.round(diffMs / 60000);
    const diffHours = Math.round(diffMs / 3600000);
    const diffDays = Math.round(diffMs / 86400000);
    const timeStr = date.toLocaleTimeString(undefined, {
        hour: "2-digit",
        minute: "2-digit",
    });
    const dateStr = date.toLocaleDateString(undefined, {
        weekday: "short",
        month: "short",
        day: "numeric",
    });
    let relative;
    if (diffMinutes < 1) {
        relative = "in less than a minute";
    }
    else if (diffMinutes < 60) {
        relative = `in ${diffMinutes} minute${diffMinutes !== 1 ? "s" : ""}`;
    }
    else if (diffHours < 24) {
        relative = `in ${diffHours} hour${diffHours !== 1 ? "s" : ""}`;
    }
    else if (diffDays < 7) {
        relative = `in ${diffDays} day${diffDays !== 1 ? "s" : ""}`;
    }
    else {
        relative = `on ${dateStr}`;
    }
    return `${dateStr} at ${timeStr} (${relative})`;
}

export { CRON_PRESETS, DAY_NAMES, DAY_SHORT_NAMES, MONTH_NAMES, cronToDescription, fieldsToCron, formatNextRun, getDayOfMonthOptions, getDayOfWeekOptions, getHourOptions, getMinuteOptions, getMonthOptions, getNextRuns, parseCron, validateCron };
