package com.capgemini.reports.quartz;

import com.capgemini.framework.common.QuartzHelper;
import com.capgemini.reports.ReportEnum;
import com.capgemini.reports.ReportLaunchTypeEnum;
import com.capgemini.reports.database.javabean.DbSchedulingReport;
import com.capgemini.reports.database.manager.MngSchedulingReport;
import com.capgemini.reports.exceptions.InvalidInputException;
import com.capgemini.reports.execution.AbstractExecutionReport;
import com.capgemini.reports.execution.posting.SchedulingPostingStrategy;
import com.capgemini.reports.periodicity.SchedulingPeriod;
import com.capgemini.reports.quartz.jobs.PurgeJob;
import com.google.common.base.Optional;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.Session;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Calendar;
import java.util.Map.Entry;

/**
 * Class representing a delayed Scheduler.<br>
 * This class delayed a job (launching of reports) to a Date parameter.
 */
@Singleton
@Startup
@Slf4j
@Getter
public class SchedulerServer {

  private static final int PLANIF_EXECUTION_SECONDS = 0;
  private static final int PLANIF_EXECUTION_MINUTES = 0;
  private static final int PLANIF_EXECUTION_HOUR = 6;

  // Internal constants
  private static final ReportLaunchTypeEnum SCHEDULED_LAUNCH = ReportLaunchTypeEnum.SCHEDULED;

  // Singleton instance
  private Scheduler scheduler;

  /**
   * Initialize the Scheduler server.
   *
   * @throws SchedulerException when unable to initialize the scheduler
   */
  @PostConstruct
  public void init() throws SchedulerException {
    log.info("Initializing Quartz scheduler");

    // Grab the Scheduler instance from the Factory
    try {
      // Initialize the Scheduler via the Kurt's quartz.properties file
      scheduler = new StdSchedulerFactory(QuartzHelper.getPropertiesFilePath()).getScheduler();
    }
    catch (SchedulerException e) {
      throw new SchedulerException("Unable to findAll an instance of the default Scheduler", e);
    }

    // Start the scheduler
    scheduler.start();
    createOrUpdatePurgeJob();

    // Recalled all the waiting jobs for persistent jobs
    try {
      scheduler.resumeAll();
    }
    catch (SchedulerException e) {
      throw new SchedulerException("Unable to start the recover the misfires job and triggers", e);
    }
  }

  private void createOrUpdatePurgeJob() throws SchedulerException {
    log.info("Checking purge job status...");
    JobKey purgeKey = PurgeJob.getPurgeJobKey();

    // update existing purge job
    if (scheduler.checkExists(purgeKey)) {
      log.info("Purge job already exists. deleting...");
      try {
        scheduler.deleteJob(purgeKey);
        log.info("Old Purge job has been deleted");
      }
      catch (SchedulerException e) {
        String jobName = purgeKey.getName();
        throw new SchedulerException("Unable to delete old " + jobName, e);
      }
    }

    // Now create purge job from scratch
    log.info("Creating purge job");
    JobDetail purgeJobDetail = PurgeJob.getPurgeJobDetail();
    Trigger triggerPurge = PurgeJob.getScheduledTrigger(purgeJobDetail);
    try {
      scheduler.scheduleJob(purgeJobDetail, triggerPurge);
      log.info("Purge job is created, next fire time: {}", triggerPurge.getNextFireTime());
    }
    catch (SchedulerException e) {
      String jobName = purgeJobDetail.getKey().getName();
      throw new SchedulerException("Unable to create " + jobName, e);
    }
  }

  /**
   * Schedule a Report to a Date parameter.
   *
   * @param job     {@link JobDetail} - the job to execute after the scheduling
   * @param trigger {@link Trigger} - the prepared trigger for the scheduled
   * @throws SchedulerException when unable to schedule the job
   */
  public void schedule(final JobDetail job, final Trigger trigger) throws SchedulerException {
    Date startDate = Calendar.getInstance().getTime();
    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
    Date nextStart = trigger.getNextFireTime();

    log.info("Current time : {}", sdf.format(startDate));
    if (nextStart == null) {
      log.info("The rapport will never trigger.");
    }
    else {
      log.info("The rapport should first strigger at {}.", sdf.format(nextStart));
    }


    // Tell quartz to schedule the job using the trigger
    scheduler.scheduleJob(job, trigger);
  }

  /**
   * De-schedule a specific job and all his triggers.
   *
   * @param jobName   String - the unique name of the job
   * @param groupName String - the group name of the job
   * @throws SchedulerException when unable to deschedule the job
   */
  public void deschedule(final String jobName, final String groupName) throws SchedulerException {
    try {
      // One report is associated to one job
      scheduler.deleteJob(new JobKey(jobName, groupName));
    }
    catch (SchedulerException e) {
      throw new SchedulerException("The job " + jobName + " " + groupName + " cannot be descheduled.", e);
    }
  }

  /**
   * Destroy the Scheduler server.
   *
   * @throws SchedulerException when a problem occurs during shutdown
   */
  @PreDestroy
  public void destroy() throws SchedulerException {
    log.info("Destroying Quartz scheduler");
    if (scheduler != null) {
      try {
        scheduler.shutdown(true);
      }
      catch (SchedulerException e) {
        throw new SchedulerException("Unable to shut down the instance of the default Scheduler", e);
      }
    }
  }

  /**
   * Deletes a job and all its triggers.
   *
   * @param schedulingReportId the ID of the scheduling report (same as DB id)
   * @throws SchedulerException if anything goes wrong with Quartz
   */
  public void deleteScheduledJobAndKeys(final Long schedulingReportId) throws SchedulerException {
    JobKey oldJobKey = TriggerPreparator.getScheduledJobKey(schedulingReportId);
    scheduler.deleteJob(oldJobKey);
  }

  /**
   * Updates the functional and temporal execution data of a job, by modifying its JobDetails and Trigger (in Quartz only, same thing should be done you in the DB).
   *
   * @param schedulingReportId the id of the scheduling to modify in Quartz
   * @param report             the new parameters are contained in this {@link AbstractExecutionReport}
   * @param priorityCode       [1-3] the new report priority
   * @param period             the new periodicity criteria
   * @param planifName         the name of the scheduling
   * @throws SchedulerException if anything goes wrong with Quartz
   */
  public void replaceJob(final Long schedulingReportId, final AbstractExecutionReport<?, ?> report, final int priorityCode,
                         final SchedulingPeriod period, final String planifName) throws SchedulerException {
    JobKey oldJobKey = TriggerPreparator.getScheduledJobKey(schedulingReportId);

    // Clean up all traces of this job
    scheduler.deleteJob(oldJobKey);

    // Now re-create it all
    Trigger trigger;
    try {
      trigger = TriggerPreparator.prepareScheduledTrigger(schedulingReportId, priorityCode, planifName, period);
    }
    catch (InvalidInputException e) {
      throw new SchedulerException("Unable to understand the scheduling parameters", e);
    }
    JobDetail jobDetail = SchedulingPostingStrategy.getScheduledJobDetail(report, trigger.getJobKey());
    scheduler.scheduleJob(jobDetail, trigger);

  }

  /**
   * Updates the trigger of a job (in Quartz only, same thing should be done in the DB).
   *
   * @param schedulingReportId the id of the scheduling to modify in Quartz
   * @param planifName         the new scheduling name
   * @param priorityCode       [1-3] the new report priority
   * @param period             the new periodicity criteria
   * @throws SchedulerException if anything goes wrong with Quartz
   */
  public void updateTemporalParameters(final Long schedulingReportId, final String planifName, final int priorityCode,
                                       final SchedulingPeriod period) throws SchedulerException {
    TriggerKey oldTriggerKey = TriggerPreparator.getScheduledTriggerKey(schedulingReportId);
    Trigger newTrigger;
    try {
      newTrigger = TriggerPreparator.prepareScheduledTrigger(schedulingReportId, priorityCode, planifName, period);
    }
    catch (InvalidInputException e) {
      throw new SchedulerException("Unable to understand the scheduling parameters", e);
    }
    scheduler.rescheduleJob(oldTriggerKey, newTrigger);
  }

  /**
   * Returns the next {@link Date} at which the job will be triggered.<br>
   * If no firing will happen in the future (no triggers left, or all triggers are in the past), returns null.
   *
   * @param schedulingReportId the id of the scheduling to investigate.
   * @return the next firing time. May be null if no trigger fires in the future.
   * @throws SchedulerException if anything goes wrong with Quartz
   */
  public Optional<Date> nextExecutionOfJob(final Long schedulingReportId) throws SchedulerException {
    final JobKey jobKey = TriggerPreparator.getScheduledJobKey(schedulingReportId);
    List<? extends Trigger> jobKeys = scheduler.getTriggersOfJob(jobKey);
    Date nextDate = null;
    for (Trigger trigger : jobKeys) {
      final Date nextFireTime = trigger.getNextFireTime();
      if (nextDate == null || nextFireTime.before(nextDate)) {
        nextDate = nextFireTime;
      }
    }
    return Optional.fromNullable(nextDate);
  }

  /**
   * Lists all jobs that will be executed tonight.
   */
  public List<JobExecution> listAllScheduledJobsForTonight(Session session) throws SchedulerException {
    GroupMatcher<JobKey> matchSchedulingGroup = GroupMatcher.jobGroupEquals(SCHEDULED_LAUNCH.getGroupName());
    Set<JobKey> jobKeys = scheduler.getJobKeys(matchSchedulingGroup);

    // Prepare a date representing tonight (actually tomorrow at 6am so we findAll all nightly executions)
    Calendar calTonight = Calendar.getInstance();
    calTonight.add(Calendar.DAY_OF_MONTH, 1); // Tomorrow before opening hours
    calTonight.set(Calendar.HOUR_OF_DAY, PLANIF_EXECUTION_HOUR);
    calTonight.set(Calendar.MINUTE, PLANIF_EXECUTION_MINUTES);
    calTonight.set(Calendar.SECOND, PLANIF_EXECUTION_SECONDS);
    final Date tonight = calTonight.getTime();

    // Request all jobKeys firing before tonight
    Map<JobKey, Trigger> firingTonight = new HashMap<>();
    for (JobKey jobKey : jobKeys) {
      List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
      for (Trigger trigger : triggers) {
        if (trigger.getNextFireTime().before(tonight)) {
          firingTonight.put(jobKey, trigger);
          break;
        }
      }
    }

    // Obtain additional data
    List<JobExecution> results = new ArrayList<>(firingTonight.size());
    for (Entry<JobKey, Trigger> jobFiring : firingTonight.entrySet()) {
      JobDetail jobDetail = scheduler.getJobDetail(jobFiring.getKey());
      long schedulingId = jobDetail.getJobDataMap().getLong(JobDataKeys.SCHEDULING_ID.toString());
      DbSchedulingReport reportSchedulingEntry = new MngSchedulingReport().searchById(session, schedulingId);
      String reportCode = reportSchedulingEntry.getReportCode();
      ReportEnum reportEnum = ReportEnum.fromCode(reportCode);
      final String planifName = jobFiring.getValue().getDescription(); // Scheduling name is stored in its description
      JobExecution jobExec = new JobExecution(reportEnum, planifName);
      results.add(jobExec);
    }

    return results;
  }

  /**
   * Simple holder to carry a report execution's detail: the {@link ReportEnum}, and the scheduling name.
   */
  @Getter
  @AllArgsConstructor
  public static class JobExecution {
    private final ReportEnum reportCode;
    private final String planificationName;
  }
}
