package com.capgemini.reports.quartz.jobs;

import com.capgemini.framework.common.TeamcenterDateFormatter;
import com.capgemini.framework.common.activedirectory.ActiveDirectory;
import com.capgemini.reports.builder.ReportBuilder;
import com.capgemini.reports.database.DataBaseReportHelper;
import com.capgemini.reports.database.HibernateUtil;
import com.capgemini.reports.database.javabean.DbReport;
import com.capgemini.reports.database.javabean.DbSchedulingReport;
import com.capgemini.reports.database.javabean.DbUser;
import com.capgemini.reports.database.javabean.enumeration.ReportCriteriaEnum;
import com.capgemini.reports.database.manager.MngCriteriaVal.CriteriaValue;
import com.capgemini.reports.database.manager.MngSchedulingCriteriaVal;
import com.capgemini.reports.database.manager.MngSchedulingReport;
import com.capgemini.reports.exceptions.InsufficientParametersException;
import com.capgemini.reports.exceptions.InvalidUserInputException;
import com.capgemini.reports.execution.AbstractExecutionReport;
import com.capgemini.reports.execution.strategies.ScheduledExecutionStrategy;
import com.capgemini.reports.logging.ReportLoggerHelper;
import com.capgemini.reports.quartz.JobDataKeys;
import com.capgemini.reports.queuing.EmbeddedServer;
import com.google.common.base.Joiner;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.quartz.*;

import javax.jms.JMSException;
import javax.naming.NamingException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;

/**
 * This Quartz {@link Job} begins the execution of a scheduled report according to a template.<br>
 * The {@link ScheduledJob#execute(JobExecutionContext) execute} method will be invoked automatically by Quartz when the time is right.
 */
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
@Slf4j
// Required for Quartz
@NoArgsConstructor
public class ScheduledJob implements Job, Serializable {

  private static final long serialVersionUID = -7297770969201094324L;

  private static Long getTemplateID(JobDataMap dataMap) throws JobExecutionException {
    if (!dataMap.containsKey(JobDataKeys.SCHEDULING_ID.toString())) {
      throw new JobExecutionException("Missing template ID for ScheduledJob execution");
    }
    else {
      return dataMap.getLong(JobDataKeys.SCHEDULING_ID.toString());
    }
  }

  /**
   * Configure the {@link JobBuilder} to execute a report.
   *
   * @param builder                the quartz {@link JobBuilder} to configure
   * @param templateID             the scheduling ID
   * @param reportBuilderClassName the name of the class to use to instantiate the report using DB data.
   */
  public static void prepareData(JobBuilder builder, Long templateID, String reportBuilderClassName) {
    builder.usingJobData(JobDataKeys.SCHEDULING_ID.toString(), templateID).usingJobData(JobDataKeys.REPORT_BUILDER_CLASS_NAME.toString(),
      reportBuilderClassName);
  }

  /**
   * Executes the requested job. The job is a scheduled report's execution. <br>
   * This method is triggered automatically by Quartz when the time comes.
   * <p>
   * An {@link AbstractExecutionReport} instance is obtained from the {@link JobExecutionContext}, which represent the report template.<br>
   * It is cloned, the quick checks are performed, and if successful the report is sent to the HornetQ queue.<br>
   * If the quick checks are unsuccessful, an email is sent to every admin.
   *
   * @param jobExecutionContext a {@link JobExecutionContext} containing a {@link JobDataMap} that explains what report to execute.
   * @throws JobExecutionException when the job could not be performed
   */
  @Override
  public void execute(final JobExecutionContext jobExecutionContext) throws JobExecutionException {
    SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
    Session session = sessionFactory.openSession();
    try {
      // Obtain general info on his job & scheduling
      JobDataMap dataMap = jobExecutionContext.getMergedJobDataMap();
      log.trace("Received a ScheduledJob request: " + Joiner.on(", ").withKeyValueSeparator("=").join(dataMap.getWrappedMap()));
      Long templateID = getTemplateID(dataMap);
      DbSchedulingReport dbTemplate = new MngSchedulingReport().searchById(session, templateID);

      // Get report params from DB
      Map<ReportCriteriaEnum, String> templateData = new MngSchedulingCriteriaVal().getFunctionalMap(templateID);

      // Construct the Report
      AbstractExecutionReport<?, ?> report = constructReport(dataMap, templateData, dbTemplate);

      log.trace("ScheduledJob: Received report template: {}", dbTemplate.getSchedulingName());

      report.setExecutionStrategy(new ScheduledExecutionStrategy()); // Override default execution strategy so emails are redirected to admins etc.
      log.trace("ScheduledJob: ExecutionStrategy set");

      // Find the name of the scheduling
      String schedulingName = dbTemplate.getSchedulingName();
      // Register scheduling name to treat specific scheduled behaviors without called database
      report.setScheduling(templateID, schedulingName);

      // Register current time as the posting time
      report.setPostingTime(TeamcenterDateFormatter.getCurrentTime());

      // Others parameters of the report
      List<CriteriaValue> launchData = report.getLaunchData();
      String reportCode = report.getReportIdentifier().getReportCode();
      String reportUid = report.getReportTechnicalUid();
      log.info("Server received a request to perform the scheduled {} report {}  with parameters: {}", reportCode, reportUid,
        Joiner.on(',').join(launchData));

      // All the administrators should be subscribed to a scheduled report
      List<DbUser> dbAdministrators = ActiveDirectory.getAdminsReportUpdateDatabase();
      log.trace("ScheduledJob: Listed {} administrators", dbAdministrators.size());

      // Save progress in database
      // Record the report launch in the database, and log this
      // The user recorded in database is the user which has submitted the report
      // Record the report launch in the database, and log this
      DbReport theReportEntry = DataBaseReportHelper.registerScheduledLaunch(report, dbAdministrators, templateID);
      log.trace("ScheduledJob: Scheduling recorded in DB");

      // Log the report request
      ReportLoggerHelper.logSchedulingRequest(theReportEntry, report);

      // Put the report in the queues for immediate execution
      postReportInQueue(report);
      log.trace("ScheduledJob: Scheduling posted in queue. Job done.");
    }
    catch (JobExecutionException e) {
      log.error("An error occurred while execution Scheduled Job", e);
      throw e;
    }
    catch (InvalidUserInputException e) {
      log.error(e.getMessage(), e);
    }
    finally {
      session.close();
    }
  }

  private AbstractExecutionReport<?, ?> constructReport(JobDataMap dataMap, Map<ReportCriteriaEnum, String> templateData,
                                                        DbSchedulingReport dbTemplate) throws JobExecutionException {
    // Obtain the class name
    String reportBuilderClassName = (String) dataMap.get(JobDataKeys.REPORT_BUILDER_CLASS_NAME.toString());
    ReportBuilder builder = obtainBuilder(reportBuilderClassName);
    try {
      return builder.build(templateData);
    }
    catch (InsufficientParametersException e) {
      throw new JobExecutionException("Insufficient parameters to build a valid report from template " + dbTemplate.getId() +
        " (" + dbTemplate.getReportCode() + ")", e);
    }
  }

  @SuppressWarnings("unchecked")
  private ReportBuilder obtainBuilder(String reportBuilderClassName) throws JobExecutionException {
    // Obtain the incoming Class object.
    Class<?> theClass;
    try {
      theClass = Class.forName(reportBuilderClassName);
    }
    catch (SecurityException | ClassNotFoundException e) {
      throw new JobExecutionException("Unable to find a class with name " + reportBuilderClassName, e);
    }

    // Cast to the ReportBuilder class.
    Class<? extends ReportBuilder> reportBuilderClass;
    try {
      reportBuilderClass = (Class<? extends ReportBuilder>) theClass;
    }
    catch (ClassCastException e) {
      throw new JobExecutionException(
        "The provided class " + reportBuilderClassName + " is not a sub-class of " + ReportBuilder.class.getName(), e);
    }
    // Request a constructor (no-args, no Class params)
    Constructor<? extends ReportBuilder> cstr;
    try {
      cstr = reportBuilderClass.getConstructor(); // Requesting a no-args constructor!
    }
    catch (SecurityException | NoSuchMethodException e) {
      throw new JobExecutionException("Unable to locate a no-args constructor for " + reportBuilderClassName, e);
    }

    // Obtain an instance of the Builder
    try {
      return cstr.newInstance();
    }
    catch (SecurityException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
      throw new JobExecutionException("Unable to create an instance from constructor of " + reportBuilderClassName, e);
    }
  }

  private void postReportInQueue(final AbstractExecutionReport<?, ?> report) throws JobExecutionException {
    try {
      EmbeddedServer.produce(report);
    }
    catch (JMSException | NamingException e) {
      String msg = "Unable to send the report in the queues.";
      log.error(msg, e);
      throw new JobExecutionException(msg, e);
    }
  }

}
