"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasValidTimezone = hasValidTimezone;
exports.hasValidSchedule = hasValidSchedule;
exports.isScheduledNow = isScheduledNow;
const tslib_1 = require("tslib");
const later_1 = tslib_1.__importDefault(require("@breejs/later"));
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const cron_parser_1 = require("cron-parser");
const cronstrue_1 = tslib_1.__importDefault(require("cronstrue"));
const luxon_1 = require("luxon");
const migration_1 = require("../../../../config/migration");
const logger_1 = require("../../../../logger");
const minutesChar = '*';
const scheduleMappings = {
    'every month': 'before 5am on the first day of the month',
    monthly: 'before 5am on the first day of the month',
};
function parseCron(scheduleText, timezone) {
    try {
        return (0, cron_parser_1.parseExpression)(scheduleText, { tz: timezone });
    }
    catch {
        return undefined;
    }
}
function hasValidTimezone(timezone) {
    if (!luxon_1.DateTime.local().setZone(timezone).isValid) {
        return [false, `Invalid schedule: Unsupported timezone ${timezone}`];
    }
    return [true];
}
function hasValidSchedule(schedule) {
    let message = '';
    if (!schedule ||
        schedule === 'at any time' ||
        schedule[0] === 'at any time') {
        return [true];
    }
    // check if any of the schedules fail to parse
    const hasFailedSchedules = schedule.some((scheduleText) => {
        const parsedCron = parseCron(scheduleText);
        if (parsedCron !== undefined) {
            if (parsedCron.fields.minute.length !== 60 ||
                scheduleText.indexOf(minutesChar) !== 0) {
                message = `Invalid schedule: "${scheduleText}" has cron syntax, but doesn't have * as minutes`;
                return true;
            }
            // It was valid cron syntax and * as minutes
            return false;
        }
        const massagedText = (0, migration_1.fixShortHours)(scheduleMappings[scheduleText] || scheduleText);
        const parsedSchedule = later_1.default.parse.text(massagedText);
        if (parsedSchedule.error !== -1) {
            message = `Invalid schedule: Failed to parse "${scheduleText}"`;
            // It failed to parse
            return true;
        }
        if (parsedSchedule.schedules.some((s) => s.m)) {
            message = `Invalid schedule: "${scheduleText}" should not specify minutes`;
            return true;
        }
        if (!parsedSchedule.schedules.some((s) => !!s.M || s.d !== undefined || !!s.D || s.t_a !== undefined || !!s.t_b)) {
            message = `Invalid schedule: "${scheduleText}" has no months, days of week or time of day`;
            return true;
        }
        // It must be OK
        return false;
    });
    if (hasFailedSchedules) {
        // If any fail then we invalidate the whole thing
        return [false, message];
    }
    return [true];
}
function cronMatches(cron, now, timezone) {
    const parsedCron = parseCron(cron, timezone);
    // it will always parse because it is checked beforehand
    // istanbul ignore if
    if (!parsedCron) {
        return false;
    }
    if (parsedCron.fields.hour.indexOf(now.hour) === -1) {
        // Hours mismatch
        return false;
    }
    if (parsedCron.fields.dayOfMonth.indexOf(now.day) === -1) {
        // Days mismatch
        return false;
    }
    if (!parsedCron.fields.dayOfWeek.includes((now.weekday % 7))) {
        // Weekdays mismatch
        return false;
    }
    if (parsedCron.fields.month.indexOf(now.month) === -1) {
        // Months mismatch
        return false;
    }
    // Match
    return true;
}
function isScheduledNow(config, scheduleKey = 'schedule') {
    let configSchedule = config[scheduleKey];
    logger_1.logger.debug(
    // TODO: types (#22198)
    `Checking schedule(schedule=${String(configSchedule)}, tz=${config.timezone}, now=${new Date().toISOString()})`);
    if (!configSchedule ||
        configSchedule.length === 0 ||
        configSchedule[0] === '' ||
        configSchedule[0] === 'at any time') {
        logger_1.logger.debug('No schedule defined');
        return true;
    }
    if (!is_1.default.array(configSchedule)) {
        logger_1.logger.warn(`config schedule is not an array: ${JSON.stringify(configSchedule)}`);
        configSchedule = [configSchedule];
    }
    const validSchedule = hasValidSchedule(configSchedule);
    if (!validSchedule[0]) {
        logger_1.logger.warn(validSchedule[1]);
        return true;
    }
    let now = luxon_1.DateTime.local();
    logger_1.logger.trace(`now=${now.toISO()}`);
    // Adjust the time if repo is in a different timezone to renovate
    if (config.timezone) {
        logger_1.logger.debug(`Found timezone: ${config.timezone}`);
        const validTimezone = hasValidTimezone(config.timezone);
        if (!validTimezone[0]) {
            logger_1.logger.warn(validTimezone[1]);
            return true;
        }
        logger_1.logger.debug('Adjusting now for timezone');
        now = now.setZone(config.timezone);
        logger_1.logger.trace(`now=${now.toISO()}`);
    }
    const currentDay = now.weekday;
    logger_1.logger.trace(`currentDay=${currentDay}`);
    // Get the number of seconds since midnight
    const currentSeconds = now
        .startOf('second')
        .diff(now.startOf('day'), 'seconds').seconds;
    logger_1.logger.trace(`currentSeconds=${currentSeconds}`);
    // Support a single string but massage to array for processing
    logger_1.logger.debug(`Checking ${configSchedule.length} schedule(s)`);
    // later is timezone agnostic (as in, it purely relies on the underlying UTC date/time that is stored in the Date),
    // which means we have to pass it a Date that has an underlying UTC date/time in the same timezone as the schedule
    const jsNow = now.setZone('utc', { keepLocalTime: true }).toJSDate();
    // We run if any schedule matches
    const isWithinSchedule = configSchedule.some((scheduleText) => {
        const cronSchedule = parseCron(scheduleText);
        if (cronSchedule) {
            const cronScheduleSummary = cronstrue_1.default.toString(scheduleText, {
                throwExceptionOnParseError: false,
            });
            logger_1.logger.debug(`Human-readable summary for cron:: ${cronScheduleSummary}`);
            // We have Cron syntax
            if (cronMatches(scheduleText, now, config.timezone)) {
                logger_1.logger.debug(`Matches schedule ${scheduleText}`);
                return true;
            }
        }
        else {
            // We have Later syntax
            const massagedText = scheduleMappings[scheduleText] || scheduleText;
            const parsedSchedule = later_1.default.parse.text((0, migration_1.fixShortHours)(massagedText));
            logger_1.logger.debug({ parsedSchedule }, `Checking schedule "${scheduleText}"`);
            if (later_1.default.schedule(parsedSchedule).isValid(jsNow)) {
                logger_1.logger.debug(`Matches schedule ${scheduleText}`);
                return true;
            }
        }
        return false;
    });
    if (!isWithinSchedule) {
        logger_1.logger.debug('Package not scheduled');
        return false;
    }
    return true;
}
//# sourceMappingURL=schedule.js.map