// backup-cron-schedule.js — standalone cron-next utility (BackupPage prototype).
// Parses a 5-field crontab + computes the next fire time for the dashboard's
// "next run" hint. Extracted verbatim from backup-page.js; loaded AFTER it.
Object.assign(BackupPage.prototype, {
    // formatRelative's future-tense sibling — "in 6h", "tomorrow at 5am", etc.
    // Used by the backup-status header to summarise the next scheduled run.
    formatRelativeFuture(when) {
        if (!when) return '';
        const t = when.getTime();
        const diff = t - Date.now();
        if (diff <= 0) return 'imminent';
        const s = Math.floor(diff / 1000);
        if (s < 60) return 'in less than a minute';
        const m = Math.floor(s / 60);
        if (m < 60) return `in ${m}m`;
        const h = Math.floor(m / 60);
        if (h < 24) return `in ${h}h`;
        const sameDay = (a, b) => a.toDateString() === b.toDateString();
        const tomorrow = new Date(Date.now() + 86400000);
        if (sameDay(when, tomorrow)) return 'tomorrow';
        const days = Math.floor(h / 24);
        return `in ${days}d`;
    },

    // "05:00" or "Mon 05:00" depending on whether it's later today or not.
    formatScheduleClock(when) {
        if (!when) return '';
        const sameDay = (new Date()).toDateString() === when.toDateString();
        const t = when.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
        if (sameDay) return `at ${t}`;
        const day = when.toLocaleDateString([], { weekday: 'short' });
        return `${day} ${t}`;
    },

    // Tiny cron-next utility — given a 5-field crontab expression
    // (minute hour dom month dow) returns a Date for the next fire after
    // now, or null if the expression is unparseable / never fires within
    // the lookahead window. Supports the common syntax: *, N, lists
    // (N,M,O), ranges (N-M), and steps (* /N or N-M/S). Doesn't try to
    // be a full cron implementation — just enough for the
    // CFG_BACKUP_CRONTAB_APP value the WebUI shows in the header.
    nextCronFireTime(expr) {
        const fields = String(expr || '').trim().split(/\s+/);
        if (fields.length !== 5) return null;
        const ranges = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
        let sets;
        try {
            sets = fields.map((f, i) => this._cronFieldSet(f, ranges[i][0], ranges[i][1]));
        } catch (_) { return null; }
        if (sets.some(s => !s.length)) return null;

        const [minSet, hourSet, domSet, monSet, dowSet] = sets;
        const minOk  = new Set(minSet);
        const hourOk = new Set(hourSet);
        const domOk  = new Set(domSet);
        const monOk  = new Set(monSet);
        const dowOk  = new Set(dowSet);
        const domStar = fields[2] === '*';
        const dowStar = fields[4] === '*';

        // Start one minute from now (rounded down to the minute) and walk
        // forward checking each candidate. Cap at ~366 days so a totally
        // unmatchable expression doesn't loop forever.
        const start = new Date();
        start.setSeconds(0, 0);
        start.setMinutes(start.getMinutes() + 1);
        const limit = start.getTime() + 366 * 86400 * 1000;
        const cur = new Date(start);
        while (cur.getTime() < limit) {
            const m = cur.getMinutes();
            const h = cur.getHours();
            const dom = cur.getDate();
            const mon = cur.getMonth() + 1;
            const dow = cur.getDay();   // 0 = Sun
            // POSIX cron: if both DOM and DOW are restricted, fire when
            // EITHER matches. If only one is restricted, that one must
            // match. If both are *, day passes.
            let dayMatch;
            if (domStar && dowStar)       dayMatch = true;
            else if (domStar)             dayMatch = dowOk.has(dow);
            else if (dowStar)             dayMatch = domOk.has(dom);
            else                          dayMatch = domOk.has(dom) || dowOk.has(dow);
            if (dayMatch && monOk.has(mon) && hourOk.has(h) && minOk.has(m)) {
                return new Date(cur);
            }
            cur.setMinutes(cur.getMinutes() + 1);
        }
        return null;
    },

    // Expand one cron field into a sorted list of valid numeric values.
    // Throws on bad syntax so nextCronFireTime can drop back to null.
    _cronFieldSet(field, lo, hi) {
        const out = new Set();
        for (const part of String(field).split(',')) {
            // step (every-Nth): "value/step" — value is "*", a single
            // number, or a range "a-b".
            let stepBase = part, step = 1;
            const slash = part.indexOf('/');
            if (slash !== -1) {
                stepBase = part.slice(0, slash);
                step = parseInt(part.slice(slash + 1), 10);
                if (!Number.isFinite(step) || step < 1) throw new Error('bad step');
            }
            let from = lo, to = hi;
            if (stepBase === '*' || stepBase === '') {
                // range stays lo..hi
            } else if (stepBase.includes('-')) {
                const [a, b] = stepBase.split('-').map(n => parseInt(n, 10));
                if (!Number.isFinite(a) || !Number.isFinite(b)) throw new Error('bad range');
                from = a; to = b;
            } else {
                const n = parseInt(stepBase, 10);
                if (!Number.isFinite(n)) throw new Error('bad value');
                from = n; to = n;
            }
            if (from < lo || to > hi || from > to) throw new Error('out of range');
            for (let v = from; v <= to; v += step) out.add(v);
        }
        return [...out].sort((a, b) => a - b);
    },
});
