const modulename = 'FxScheduler';
import { parseSchedule } from '@lib/misc';
import consoleFactory from '@lib/console';
import { SYM_SYSTEM_AUTHOR } from '@lib/symbols';
import type { UpdateConfigKeySet } from './ConfigStore/utils';
const console = consoleFactory(modulename);


//Types
type RestartInfo = {
    string: string;
    minuteFloorTs: number;
}
type ParsedTime = {
    string: string;
    hours: number;
    minutes: number;
}


//Consts
const scheduleWarnings = [30, 15, 10, 5, 4, 3, 2, 1];


/**
 * Processes an array of HH:MM, gets the next timestamp (sorted by closest).
 * When time matches, it will be dist: 0, distMins: 0, and nextTs likely in the 
 * past due to seconds and milliseconds being 0.
 */
const getNextScheduled = (parsedSchedule: ParsedTime[]): RestartInfo => {
    const thisMinuteTs = new Date().setSeconds(0, 0);
    const processed = parsedSchedule.map((t) => {
        const nextDate = new Date();
        let minuteFloorTs = nextDate.setHours(t.hours, t.minutes, 0, 0);
        if (minuteFloorTs < thisMinuteTs) {
            minuteFloorTs = nextDate.setHours(t.hours + 24, t.minutes, 0, 0);
        }
        return {
            string: t.string,
            minuteFloorTs,
        };
    });
    return processed.sort((a, b) => a.minuteFloorTs - b.minuteFloorTs)[0];
};


/**
 * Module responsible for restarting the FXServer on a schedule defined in the config,
 * or a temporary schedule set by the user at runtime.
 */
export default class FxScheduler {
    static readonly configKeysWatched = ['restarter.schedule'];
    private nextTempSchedule: RestartInfo | false = false;
    private calculatedNextRestartMinuteFloorTs: number | false = false;
    private nextSkip: number | false = false;

    constructor() {
        //Initial check to update status
        setImmediate(() => {
            this.checkSchedule();
        });

        //Cron Function 
        setInterval(() => {
            this.checkSchedule();
            txCore.webServer.webSocket.pushRefresh('status');
        }, 60 * 1000);
    }


    /**
     * Refresh configs, resets skip and temp scheduled, runs checkSchedule.
     */
    handleConfigUpdate(updatedConfigs: UpdateConfigKeySet) {
        this.nextSkip = false;
        this.nextTempSchedule = false;
        this.checkSchedule();
        txCore.webServer.webSocket.pushRefresh('status');
    }


    /**
     * Updates state when server closes.
     * Clear temp skips and skips next scheduled if it's in less than 2 hours.
     */
    handleServerClose() {
        //Clear temp schedule, recalculates next restart
        if (this.nextTempSchedule) this.nextTempSchedule = false;
        this.checkSchedule(true);
        
        //Check if next scheduled restart is in less than 2 hours
        const inTwoHours = Date.now() + 2 * 60 * 60 * 1000;
        if (
            this.calculatedNextRestartMinuteFloorTs 
            && this.calculatedNextRestartMinuteFloorTs < inTwoHours
            && this.nextSkip !== this.calculatedNextRestartMinuteFloorTs
        ) {
            console.warn('Server closed, skipping next scheduled restart because it\'s in less than 2 hours.');
            this.nextSkip = this.calculatedNextRestartMinuteFloorTs;
        }
        this.checkSchedule(true);

        //Push UI update
        txCore.webServer.webSocket.pushRefresh('status');
    }


    /**
     * Returns the current status of scheduler
     * NOTE: sending relative because server might have clock skew
     */
    getStatus() {
        if (this.calculatedNextRestartMinuteFloorTs) {
            const thisMinuteTs = new Date().setSeconds(0, 0);
            return {
                nextRelativeMs: this.calculatedNextRestartMinuteFloorTs - thisMinuteTs,
                nextSkip: this.nextSkip === this.calculatedNextRestartMinuteFloorTs,
                nextIsTemp: !!this.nextTempSchedule,
            };
        } else {
            return {
                nextRelativeMs: false,
                nextSkip: false,
                nextIsTemp: false,
            } as const;
        }
    }


    /**
     * Sets this.nextSkip.
     * Cancel scheduled button -> setNextSkip(true)
     * Enable scheduled button -> setNextSkip(false)
     */
    setNextSkip(enabled: boolean, author?: string) {
        if (enabled) {
            let prevMinuteFloorTs, temporary;
            if (this.nextTempSchedule) {
                prevMinuteFloorTs = this.nextTempSchedule.minuteFloorTs;
                temporary = true;
                this.nextTempSchedule = false;
            } else if (this.calculatedNextRestartMinuteFloorTs) {
                prevMinuteFloorTs = this.calculatedNextRestartMinuteFloorTs;
                temporary = false;
                this.nextSkip = this.calculatedNextRestartMinuteFloorTs;
            }

            if (prevMinuteFloorTs) {
                //Dispatch `txAdmin:events:scheduledRestartSkipped`
                txCore.fxRunner.sendEvent('scheduledRestartSkipped', {
                    secondsRemaining: Math.floor((prevMinuteFloorTs - Date.now()) / 1000),
                    temporary,
                    author,
                });

                //FIXME: deprecate
                txCore.fxRunner.sendEvent('skippedNextScheduledRestart', {
                    secondsRemaining: Math.floor((prevMinuteFloorTs - Date.now()) / 1000),
                    temporary
                });
            }
        } else {
            this.nextSkip = false;
        }

        //This is needed to refresh this.calculatedNextRestartMinuteFloorTs
        this.checkSchedule();

        //Refresh UI
        txCore.webServer.webSocket.pushRefresh('status');
    }


    /**
     * Sets this.nextTempSchedule.
     * The value MUST be before the next setting scheduled time.
     */
    setNextTempSchedule(timeString: string) {
        //Process input
        if (typeof timeString !== 'string') throw new Error('expected string');
        const thisMinuteTs = new Date().setSeconds(0, 0);
        let scheduledString, scheduledMinuteFloorTs;

        if (timeString.startsWith('+')) {
            const minutes = parseInt(timeString.slice(1));
            if (isNaN(minutes) || minutes < 1 || minutes >= 1440) {
                throw new Error('invalid minutes');
            }
            const nextDate = new Date(thisMinuteTs + (minutes * 60 * 1000));
            scheduledMinuteFloorTs = nextDate.getTime();
            scheduledString = nextDate.getHours().toString().padStart(2, '0') + ':' + nextDate.getMinutes().toString().padStart(2, '0');
        } else {
            const [hours, minutes] = timeString.split(':', 2).map((x) => parseInt(x));
            if (typeof hours === 'undefined' || isNaN(hours) || hours < 0 || hours > 23) throw new Error('invalid hours');
            if (typeof minutes === 'undefined' || isNaN(minutes) || minutes < 0 || minutes > 59) throw new Error('invalid minutes');

            const nextDate = new Date();
            scheduledMinuteFloorTs = nextDate.setHours(hours, minutes, 0, 0);
            if (scheduledMinuteFloorTs === thisMinuteTs) {
                throw new Error('Due to the 1 minute precision of the restart scheduler, you cannot schedule a restart in the same minute.');
            }
            if (scheduledMinuteFloorTs < thisMinuteTs) {
                scheduledMinuteFloorTs = nextDate.setHours(hours + 24, minutes, 0, 0);
            }
            scheduledString = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0');
        }

        //Check validity
        if (Array.isArray(txConfig.restarter.schedule) && txConfig.restarter.schedule.length) {
            const { valid } = parseSchedule(txConfig.restarter.schedule);
            const nextSettingRestart = getNextScheduled(valid);
            if (nextSettingRestart.minuteFloorTs < scheduledMinuteFloorTs) {
                throw new Error(`You already have one restart scheduled for ${nextSettingRestart.string}, which is before the time you specified.`);
            }
        }

        // Set next temp schedule
        this.nextTempSchedule = {
            string: scheduledString,
            minuteFloorTs: scheduledMinuteFloorTs,
        };

        //This is needed to refresh this.calculatedNextRestartMinuteFloorTs
        this.checkSchedule();

        //Refresh UI
        txCore.webServer.webSocket.pushRefresh('status');
    }


    /**
     * Checks the schedule to see if it's time to announce or restart the server
     */
    async checkSchedule(calculateOnly = false) {
        //Check settings and temp scheduled restart
        let nextRestart: RestartInfo;
        if (this.nextTempSchedule) {
            nextRestart = this.nextTempSchedule;
        } else if (Array.isArray(txConfig.restarter.schedule) && txConfig.restarter.schedule.length) {
            const { valid } = parseSchedule(txConfig.restarter.schedule);
            nextRestart = getNextScheduled(valid);
        } else {
            //nothing scheduled
            this.calculatedNextRestartMinuteFloorTs = false;
            return;
        }
        this.calculatedNextRestartMinuteFloorTs = nextRestart.minuteFloorTs;
        if (calculateOnly) return;

        //Checking if skipped
        if (this.nextSkip === this.calculatedNextRestartMinuteFloorTs) {
            console.verbose.log(`Skipping next scheduled restart`);
            return;
        }

        //Calculating dist
        const thisMinuteTs = new Date().setSeconds(0, 0);
        const nextDistMs = nextRestart.minuteFloorTs - thisMinuteTs;
        const nextDistMins = Math.floor(nextDistMs / 60_000);

        //Checking if server restart or warning time
        if (nextDistMins === 0) {
            //restart server
            this.triggerServerRestart(
                `scheduled restart at ${nextRestart.string}`,
                txCore.translator.t('restarter.schedule_reason', { time: nextRestart.string }),
            );

            //Check if server is in boot cooldown
            const processUptime = Math.floor((txCore.fxRunner.child?.uptime ?? 0) / 1000);
            if (processUptime < txConfig.restarter.bootGracePeriod) {
                console.verbose.log(`Server is in boot cooldown, skipping scheduled restart.`);
                return;
            }

            //reset next scheduled
            this.nextTempSchedule = false;

        } else if (scheduleWarnings.includes(nextDistMins)) {
            const tOptions = {
                smart_count: nextDistMins,
                servername: txConfig.general.serverName,
            };

            //Send discord warning
            txCore.discordBot.sendAnnouncement({
                type: 'warning',
                description: {
                    key: 'restarter.schedule_warn_discord',
                    data: tOptions
                }
            });

            //Dispatch `txAdmin:events:scheduledRestart` 
            txCore.fxRunner.sendEvent('scheduledRestart', {
                secondsRemaining: nextDistMins * 60,
                translatedMessage: txCore.translator.t('restarter.schedule_warn', tOptions)
            });
        }
    }


    /**
     * Triggers FXServer restart and logs the reason.
     */
    async triggerServerRestart(reasonInternal: string, reasonTranslated: string) {
        //Sanity check
        if (txCore.fxRunner.isIdle || !txCore.fxRunner.child?.isAlive) {
            console.verbose.warn('Server not running, skipping scheduled restart.');
            return false;
        }

        //Restart server
        const logMessage = `Restarting server: ${reasonInternal}`;
        txCore.logger.admin.write('SCHEDULER', logMessage);
        txCore.logger.fxserver.logInformational(logMessage); //just for better visibility
        txCore.fxRunner.restartServer(reasonTranslated, SYM_SYSTEM_AUTHOR);
    }
};
