package com.capgemini.reports.quartz;

import com.capgemini.framework.common.SettingsHelper;
import com.capgemini.reports.ReportLaunchTypeEnum;
import com.capgemini.reports.exceptions.InvalidInputException;
import com.capgemini.reports.periodicity.*;
import com.capgemini.reports.periodicity.SchedulingPeriod.PeriodVisitor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

/**
 * Prepare triggers for the {@link SchedulerServer}.
 */
@Slf4j
public final class TriggerPreparator implements PeriodVisitor<Trigger> {

    // Weekend numbers
    private static final int NUM_WEEK_5 = 5;
    private static final int NUM_WEEK_4 = 4;
    private static final int NUM_WEEK_3 = 3;
    private static final int NUM_WEEK_2 = 2;
    private static final int NUM_WEEK_1 = 1;

    // PSnecma Priority scale
    private static final int SNECMA_LOW_PRIORITY = 1;
    private static final int SNECMA_HIGH_PRIORITY = 3;

    // Quartz message priority scale
    private static final int QUARTZ_LOW_PRIORITY = 1;
    private static final int QUARTZ_MEDIUM_PRIORITY = 5;
    private static final int QUARTZ_HIGH_PRIORITY = 10;

    // Others configurations
    private static final String PREF_TRIGGER_NAME = "trigger_name_";
    private static final String PREF_JOB_NAME = "job_name_";
    private static final String STR_COLON = ":";

    // Configuration for delayed launch type. Format: HH:MM
    private static final String LAUNCH_HOUR_STR;
    private static final int LAUNCH_HOUR;
    private static final int LAUNCH_MINUTE;

    static {
        LAUNCH_HOUR_STR = SettingsHelper.getDelayedLaunchHour();
        String[] tableStrLaunchTime = LAUNCH_HOUR_STR.split(STR_COLON);
        LAUNCH_HOUR = Integer.parseInt(tableStrLaunchTime[0]);
        LAUNCH_MINUTE = Integer.parseInt(tableStrLaunchTime[1]);
    }

    private final int reportPriority;
    private final String strSchedulName;
    /**
     * The ID of the Scheduling as it appears in the DB.
     */
    private final Long schedulingReportId;

    /**
     * Private constructor - use static methods to obtain an instance.
     *
     * @param reportPriority     the report priority
     * @param strSchedulName
     * @param schedulingReportId
     */
    private TriggerPreparator(final int reportPriority, final String strSchedulName, final Long schedulingReportId) {
        this.reportPriority = reportPriority;
        this.strSchedulName = strSchedulName;
        this.schedulingReportId = schedulingReportId;
    }

    /**
     * Prepare a delayed Trigger for the Scheduler.
     *
     * @param reportUid      {@link String} - the report uid
     * @param reportPriority [1-3] - the report priority
     * @return the trigger
     */
    public static Trigger prepareDelayedTrigger(final String reportUid, final int reportPriority) {
        // In a mis-fire situation, the SimpleTrigger wants to be fired now by Scheduler
        final TriggerKey delayedTriggerKey = getDelayedTriggerKey(reportUid);
        final JobKey delayedJobKey = getDelayedJobKey(reportUid);
        final SimpleScheduleBuilder tonight = SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow();
        final int triggerPriorities = giveTriggerPriorities(reportPriority);
        return TriggerBuilder.newTrigger().withIdentity(delayedTriggerKey).startAt(makeDelayedHour(LAUNCH_HOUR_STR)).forJob(delayedJobKey)
                .withSchedule(tonight).withPriority(triggerPriorities).build();
    }

    /**
     * Prepares a Trigger for a scheduled report.
     *
     * @param schedulingReportId (Long) the ID of the report (as it appears in DB)
     * @param reportPriority     [1-3] the priority of the scheduling (1-9)
     * @param strScheduleName    the name given by the requester
     * @param period             a {@link SchedulingPeriod} for periodicity
     * @return the {@link Trigger}
     * @throws InvalidInputException if any input is null
     */
    public static Trigger prepareScheduledTrigger(final Long schedulingReportId, final int reportPriority, final String strScheduleName,
                                                  final SchedulingPeriod period) throws InvalidInputException {
        if (schedulingReportId == null) {
            throw new InvalidInputException("Invalid report Id (null)");
        }
        if (strScheduleName == null) {
            throw new InvalidInputException("Invalid schedulign name (null)");
        }
        if (period == null) {
            throw new InvalidInputException("Invalid period (null)");
        }
        return period.accept(new TriggerPreparator(reportPriority, strScheduleName, schedulingReportId));
    }

    /**
     * Prepare a Trigger for a specific date.<br>
     *
     * @param schedulingReportId Long - the report ID
     * @param reportPriority     [1-3] - the priorities of the report
     * @param strSchedulName     String - the scheduling name
     * @param schedulingDate     A string for the scheduling DATE. The format of the date is dd-mm-yyyy.
     * @return the built trigger
     */
    private static Trigger byDate(final Long schedulingReportId, final int reportPriority, final String strSchedulName, final Date schedulingDate) {
        final TriggerKey scheduledTriggerKey = getScheduledTriggerKey(schedulingReportId);
        final JobKey scheduledJobKey = getScheduledJobKey(schedulingReportId);
        final SimpleScheduleBuilder simpleSchedule = SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow();
        final int triggerPriorities = giveTriggerPriorities(reportPriority);
        return TriggerBuilder.newTrigger().withIdentity(scheduledTriggerKey).withDescription(strSchedulName).startAt(schedulingDate)
                .forJob(scheduledJobKey).withSchedule(simpleSchedule).withPriority(triggerPriorities).build();
    }

    /**
     * Prepare a scheduled Trigger for the Scheduler.<br>
     *
     * @param schedulingReportId Long - the report ID
     * @param reportPriority     [1-3] - the priorities of the report
     * @param strSchedulName     String - the scheduling name
     * @param dayInWeek          takes the value 1 to 5 for Monday to Friday. The value is 1 for Monday until 5 for Friday.
     * @return the built trigger
     */
    public static Trigger weekly(final Long schedulingReportId, final int reportPriority, final String strSchedulName, final int dayInWeek) {
        final TriggerKey scheduledTriggerKey = getScheduledTriggerKey(schedulingReportId);
        final JobKey scheduledJobKey = getScheduledJobKey(schedulingReportId);
        final CronScheduleBuilder weeklySchedule =
                CronScheduleBuilder.weeklyOnDayAndHourAndMinute(getDayInWeek(dayInWeek), LAUNCH_HOUR, LAUNCH_MINUTE);
        final int triggerPriorities = giveTriggerPriorities(reportPriority);
        return TriggerBuilder.newTrigger().withIdentity(scheduledTriggerKey).withDescription(strSchedulName).forJob(scheduledJobKey).startNow()
                .withSchedule(weeklySchedule).withPriority(triggerPriorities).build();
    }

    /**
     * Prepare a scheduled Trigger for the Scheduler.<br>
     *
     * @param schedulingReportId Long - the report ID
     * @param reportPriority     [1-3] - the priorities of the report
     * @param strSchedulName     String - the scheduling name
     * @return the built trigger
     */
    public static Trigger daily(final Long schedulingReportId, final int reportPriority, final String strSchedulName) {
        final TriggerKey scheduledTriggerKey = getScheduledTriggerKey(schedulingReportId);
        final JobKey scheduledJobKey = getScheduledJobKey(schedulingReportId);
        final CronScheduleBuilder dailySchedule = CronScheduleBuilder.dailyAtHourAndMinute(LAUNCH_HOUR, LAUNCH_MINUTE);
        final int triggerPriorities = giveTriggerPriorities(reportPriority);
        return TriggerBuilder.newTrigger().withIdentity(scheduledTriggerKey).withDescription(strSchedulName).forJob(scheduledJobKey).startNow()
                .withSchedule(dailySchedule).withPriority(triggerPriorities).build();
    }

    /**
     * Prepare a scheduled Trigger for the Scheduler.<br>
     *
     * @param schedulingReportId Long - the report ID
     * @param reportPriority     integer - the priorities of the report
     * @param strSchedulName     String - the scheduling name
     * @param monthType          An enumeration of monthly type of scheduling ({@link WeekendPeriod}). May contains an integer for DAY_IN_MONTH (the date in month). In this case, the value should be between 1 and 31.
     * @return the built trigger
     */
    private static Trigger weekend(final Long schedulingReportId, final int reportPriority, final String strSchedulName,
                                   final WeekendPeriod monthType) {
        final TriggerKey scheduledTriggerKey = getScheduledTriggerKey(schedulingReportId);
        final JobKey scheduledJobKey = getScheduledJobKey(schedulingReportId);
        final TriggerBuilder<Trigger> triggerBuilder =
                TriggerBuilder.newTrigger().withIdentity(scheduledTriggerKey).withDescription(strSchedulName).forJob(scheduledJobKey).startNow();
        final String cronExpression;
        switch (monthType) {
            case END_MONTH:
                cronExpression = cronEndOfMonth();
                break;
            case FIRST_WEEK_END:
                cronExpression = cronWeekendByNum(LAUNCH_HOUR, LAUNCH_MINUTE, NUM_WEEK_1);
                break;
            case SECOND_WEEK_END:
                cronExpression = cronWeekendByNum(LAUNCH_HOUR, LAUNCH_MINUTE, NUM_WEEK_2);
                break;
            case THIRD_WEEK_END:
                cronExpression = cronWeekendByNum(LAUNCH_HOUR, LAUNCH_MINUTE, NUM_WEEK_3);
                break;
            case FOURTH_WEEK_END:
                cronExpression = cronWeekendByNum(LAUNCH_HOUR, LAUNCH_MINUTE, NUM_WEEK_4);
                break;
            case FIFTH_WEEK_END:
                cronExpression = cronWeekendByNum(LAUNCH_HOUR, LAUNCH_MINUTE, NUM_WEEK_5);
                break;
            default:
                // This case shoudn't occur
                log.error("An error occured during the creation of the trigger because of the value of the period for the scheduled report {}",
                        schedulingReportId);
                return null;
        }
        ScheduleBuilder<CronTrigger> scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
        final int triggerPriorities = giveTriggerPriorities(reportPriority);
        return triggerBuilder.withSchedule(scheduleBuilder).withPriority(triggerPriorities).build();
    }

    private static String cronEndOfMonth() {
        return "0 " + LAUNCH_MINUTE + " " + LAUNCH_HOUR + " L * ?";
    }

    /**
     * Prepare a scheduled Trigger for the Scheduler.<br>
     *
     * @param schedulingReportId Long - the report ID
     * @param reportPriority     [1-3] - the priorities of the report
     * @param strSchedulName     String - the scheduling name
     * @param dayInMonth         an integer for DAY_IN_MONTH (the date in month). The value should be between 1 and 31
     * @return the built trigger
     */
    private static Trigger dayInMonth(final Long schedulingReportId, final int reportPriority, final String strSchedulName, final int dayInMonth) {
        final TriggerKey scheduledTriggerKey = getScheduledTriggerKey(schedulingReportId);
        final JobKey scheduledJobKey = getScheduledJobKey(schedulingReportId);
        ScheduleBuilder<CronTrigger> scheduleEveryMonth = CronScheduleBuilder.monthlyOnDayAndHourAndMinute(dayInMonth, LAUNCH_HOUR, LAUNCH_MINUTE);
        final int triggerPriorities = giveTriggerPriorities(reportPriority);
        return TriggerBuilder.newTrigger().withIdentity(scheduledTriggerKey).withDescription(strSchedulName).forJob(scheduledJobKey).startNow()
                .withSchedule(scheduleEveryMonth).withPriority(triggerPriorities).build();
    }

    private static String cronWeekendByNum(final int launchHour, final int launchMinute, final int coeffOfWeek) {
        return "0 " + launchMinute + " " + launchHour + " ? * 1#" + coeffOfWeek;
    }

    /**
     * Can return a result adapted to the scheduler to the following format :
     * <ul>
     * <li>2 for Monday.
     * <li>3 for Tuesday.
     * <li>4 for Wednesday.
     * <li>5 for Thursday.
     * <li>6 for Friday.
     * </ul>
     *
     * @param dayInWeek integer - the received integer value from the IHM
     * @return the adapted integer value
     */
    private static int getDayInWeek(final int dayInWeek) {
        return dayInWeek + 1;
    }

    private static Date makeDelayedHour(final String launchDate) {
        // Separate hours and minutes (format hour:minute)
        String[] tableStrLaunchTime = launchDate.split(STR_COLON);
        int launchHour = Integer.parseInt(tableStrLaunchTime[0]);
        int launchMinute = Integer.parseInt(tableStrLaunchTime[1]);

        // Retrieve current time
        Date startDate = Calendar.getInstance().getTime();
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
        String[] tableStrCurrentTime = sdf.format(startDate).split(STR_COLON);
        int currentHour = Integer.parseInt(tableStrCurrentTime[0]);
        int currentMinute = Integer.parseInt(tableStrCurrentTime[1]);

        // Compare launch time to current time to customize the Trigger
        Date result;
        if (launchHour > currentHour) {
            result = DateBuilder.todayAt(launchHour, launchMinute, 0);
        } else if (launchHour < currentHour) {
            result = DateBuilder.tomorrowAt(launchHour, launchMinute, 0);
        } else if (launchMinute > currentMinute) {
            result = DateBuilder.todayAt(launchHour, launchMinute, 0);
        } else {
            result = DateBuilder.tomorrowAt(launchHour, launchMinute, 0);
        }
        return result;
    }

    /**
     * Returns a scheduled report's {@link TriggerKey}. Can be used to create/retrieve a scheduled job's Trigger in Quartz.
     *
     * @param schedulingReportId the Id (long) of the report to be scheduled
     * @return the prepared {@link TriggerKey}
     */
    public static TriggerKey getScheduledTriggerKey(final Long schedulingReportId) {
        return new TriggerKey(PREF_TRIGGER_NAME + schedulingReportId, ReportLaunchTypeEnum.SCHEDULED.getGroupName());
    }

    // *********************** Instance ***************************

    /**
     * Returns a delayed report's {@link TriggerKey}. Can be used to create/retrieve a delayed job's Trigger in Quartz.
     *
     * @param delayedReportUID the UID (String) of the report to be delayed
     * @return the prepared {@link TriggerKey}
     */
    private static TriggerKey getDelayedTriggerKey(final String delayedReportUID) {
        return new TriggerKey(PREF_TRIGGER_NAME + delayedReportUID, ReportLaunchTypeEnum.DELAYED.getGroupName());
    }

    /**
     * Returns a scheduled report's {@link JobKey}. Can be used to create/retrieve a scheduled job in Quartz.
     *
     * @param schedulingReportId the Id (long) of the report to be scheduled
     * @return the prepared {@link JobKey}
     */
    public static JobKey getScheduledJobKey(final Long schedulingReportId) {
        return new JobKey(PREF_JOB_NAME + schedulingReportId, ReportLaunchTypeEnum.SCHEDULED.getGroupName());
    }

    /**
     * Returns a delayed report's {@link JobKey}. Can be used to create/retrieve a delayed job in Quartz.
     *
     * @param schedulingUID the UID (String) of the report to be scheduled
     * @return the prepared {@link JobKey}
     */
    private static JobKey getDelayedJobKey(final String schedulingUID) {
        return new JobKey(PREF_JOB_NAME + schedulingUID, ReportLaunchTypeEnum.DELAYED.getGroupName());
    }

    /**
     * Translate the priorities of the reporting.properties file into the Quartz language to the scheduler.<br>
     * For the settings file :<br>
     * <ul>
     * <li>1 correspond to a high priority
     * <li>2 correspond to a medium priority
     * <li>3 correspond to a low priority
     * </ul>
     * For a TriggerBuilder of Quartz :
     * <ul>
     * <li>1 is the highest priority
     * <li>5 is the medium priority
     * <li>10 is the lowest priority
     * </ul>
     */
    private static int giveTriggerPriorities(final int reportPriorities) {
        switch (reportPriorities) {
            case SNECMA_LOW_PRIORITY:
                return QUARTZ_LOW_PRIORITY;
            case SNECMA_HIGH_PRIORITY:
                return QUARTZ_HIGH_PRIORITY;
            default:
                return QUARTZ_MEDIUM_PRIORITY;
        }
    }

    // *********************** Visitor Pattern ***************************
    @Override
    public Trigger visit(final DayInMonth dayInMonth) {
        return dayInMonth(schedulingReportId, reportPriority, strSchedulName, dayInMonth.getDayInMonth());
    }

    @Override
    public Trigger visit(final WeekendPeriod weekend) {
        return weekend(schedulingReportId, reportPriority, strSchedulName, weekend);
    }

    @Override
    public Trigger visit(final SpecificDay specificDay) {
        return byDate(schedulingReportId, reportPriority, strSchedulName, specificDay.getDay());
    }

    @Override
    public Trigger visit(final WeeklyPeriod dayInWeek) {
        return weekly(schedulingReportId, reportPriority, strSchedulName, dayInWeek.getDayInWeek());
    }

    @Override
    public Trigger visit(final DailyPeriod uselessParam) {
        return daily(schedulingReportId, reportPriority, strSchedulName);
    }
}
