package com.airlock.iam.servicecontainer.app.application.configuration;

import com.airlock.iam.base.misc.concurrent.ThreadUtils;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.lifecycle.BeforePluginDeactivation;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.LongProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.PluginListPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import com.airlock.iam.servicecontainer.api.domain.model.task.Task;
import com.airlock.iam.servicecontainer.app.application.service.AbstractService;
import com.airlock.iam.servicecontainer.app.infrastructure.server.ManagableViaGUI;
import com.airlock.iam.servicecontainer.app.infrastructure.webapp.presenter.TaskSchedulerServicePresenter;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import org.apache.commons.lang3.Strings;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.FastDateFormat;

@Plugin(description = "The task scheduler is a service that allows regular execution of tasks.")
public class TaskSchedulerService extends AbstractService implements ManagableViaGUI {
   private static final long DEFAULT_CHECK_INTERVAL = 5000L;
   private static final int SLEEP_TIME = 1000;
   private static final ELogger LOG = ELoggerFactory.getLogger(TaskSchedulerService.class);
   private static final Lock GLOBAL_LOCK = new ReentrantLock();
   @PluginListProperty(description = "Defines the task and their scheduling information.", optional = true)
   private List<TaskSchedule> tasks;
   private static final String TASKS_PROPERTY = "tasks";
   @LongProperty(
      description = "The interval in milliseconds in which the scheduler service checks whether a task should be executed. Task execution may be late for this amount of time at most.",
      optional = true,
      minValue = 1L,
      defaultValue = 5000L,
      exampleValues = {5000L, 10000L}
   )
   private long checkIntervalMillis;
   @BooleanProperty(
      description = "If the tasks should be run concurrently or sequentially.<br>If enabled, all tasks may run concurrently. If disabled, all tasks run sequentially.<br>This configuration applies to all tasks of this scheduler.",
      defaultValue = "true",
      optional = true,
      displayName = "Run Tasks Concurrently"
   )
   private boolean runTasksConcurrently;
   private final Set<String> reportedTasks = new HashSet<>();
   private Function<TaskSchedule, Optional<Server>> serverForExecution = this::serverForExecution;

   TaskSchedulerService(List<TaskSchedule> tasks, long checkIntervalMillis, boolean runTasksConcurrently) {
      this.tasks = tasks;
      this.checkIntervalMillis = checkIntervalMillis;
      this.runTasksConcurrently = runTasksConcurrently;
   }

   @VisibleForTesting
   void setServerForExecution(Function<TaskSchedule, Optional<Server>> serverForExecution) {
      this.serverForExecution = serverForExecution;
   }

   @Override
   public void run() {
      this.setRunning(true);

      try {
         long lastCheckTime = System.currentTimeMillis();

         while (!this.isStopping()) {
            if (System.currentTimeMillis() - lastCheckTime >= this.checkIntervalMillis) {
               long now = System.currentTimeMillis();
               this.startAllScheduledTasksInInterval(lastCheckTime, now);
               lastCheckTime = now;
            }

            ThreadUtils.sleepInterruptibly(1000L, java.util.concurrent.TimeUnit.MILLISECONDS);
         }
      } finally {
         this.askAllTasksToStop();
         this.setPleaseStopToFalse();
         this.setRunning(false);
      }
   }

   private void askAllTasksToStop() {
      this.tasks.stream().filter(TaskSchedule::isRunning).forEach(t -> t.getTask().pleaseStop());
   }

   private void startAllScheduledTasksInInterval(long intervalStart, long intervalEnd) {
      Validate.isTrue(intervalStart <= intervalEnd, "intervalStart has to be smaller than intervalEnd.", new Object[0]);

      for (TaskSchedule taskSchedule : this.tasks) {
         this.startAllScheduledTaskInInterval(intervalStart, intervalEnd, taskSchedule);
         if (this.isStopping()) {
            break;
         }
      }
   }

   private void startAllScheduledTaskInInterval(long intervalStart, long intervalEnd, TaskSchedule taskSchedule) {
      long nextExecutionDate = taskSchedule.computeNextExecutionDate(intervalStart);
      this.logBeforeScheduledExecution(taskSchedule, nextExecutionDate);
      if (nextExecutionDate != -1L && nextExecutionDate < intervalEnd) {
         if (taskSchedule.isRunning()) {
            logAlreadyRunningScheduledTask(taskSchedule);
         } else {
            this.executeTask(taskSchedule);
         }
      }
   }

   private static void logAlreadyRunningScheduledTask(TaskSchedule taskSchedule) {
      LOG.info(
         LogTypes.SERVER_INFO_SYS,
         "TaskScheduler: Task \""
            + taskSchedule.getTaskName()
            + "\" should be executed again but previous execution (started at "
            + formatDate(taskSchedule.getLatestExecutionStartDate())
            + ") is still running. Task is not executed."
      );
   }

   private void logBeforeScheduledExecution(TaskSchedule taskSchedule, long nextExecutionDate) {
      if (LOG.isDebugEnabled()) {
         if (taskSchedule.isRunning()) {
            int progressPercentage = taskSchedule.getTask().getProgressPercentage();
            LOG.debug(
               "TaskScheduler: Task \""
                  + taskSchedule.getTaskName()
                  + "\" is running. Completion: "
                  + (progressPercentage < 0 ? "N/A" : progressPercentage + "%")
            );
         }

         this.logNextExecutionDate(taskSchedule, nextExecutionDate);
      }
   }

   private void logNextExecutionDate(TaskSchedule taskSchedule, long nextExecutionDate) {
      if (!this.reportedTasks.contains(taskSchedule.getTaskName())) {
         String next = nextExecutionDate == -1L ? "[not scheduled]" : formatDate(nextExecutionDate);
         LOG.debug(String.format("Next execution date for task \"%s\" is at %s", taskSchedule.getTaskName(), next));
         this.reportedTasks.add(taskSchedule.getTaskName());
      }
   }

   public static String formatDate(long date) {
      return date == 0L ? "[null]" : FastDateFormat.getInstance("dd.MM.yyyy HH:mm:ss").format(new Date(date));
   }

   public void executeTask(TaskSchedule taskSchedule) {
      this.reportedTasks.remove(taskSchedule.getTaskName());
      LOG.info(LogTypes.SERVER_INFO_SYS, "Scheduler starting task: " + taskSchedule.getTaskName());
      Optional<Server> server = this.serverForExecution.apply(taskSchedule);
      if (!server.isEmpty()) {
         boolean threadStarted = server.get().executeTask(this.potentiallySequentialTask(taskSchedule), taskSchedule.getTaskName());
         if (threadStarted) {
            taskSchedule.setStarted();
         } else {
            LOG.error(
               LogTypes.SERVER_INFO_SYS,
               "Could not obtain thread for task \""
                  + taskSchedule.getTaskName()
                  + "\". Task not started. Increase Thread-Pool Size in Server to allow for more threads."
            );
         }
      }
   }

   private Task potentiallySequentialTask(TaskSchedule taskSchedule) {
      Task task = taskSchedule.getTask();
      return this.runTasksConcurrently ? task : new SequentialTask(task, GLOBAL_LOCK);
   }

   private Optional<Server> serverForExecution(TaskSchedule taskSchedule) {
      try {
         return Optional.of(this.getServer());
      } catch (RuntimeException e) {
         LOG.warn(LogTypes.SERVER_INFO_SYS, "Exception while starting task " + LogUtil.quote(taskSchedule.getTaskName()) + ": " + e.getMessage());
         return Optional.empty();
      }
   }

   public boolean executeTask(String taskName) {
      Optional<TaskSchedule> schedule = this.findTaskScheduleByName(taskName);
      if (schedule.isPresent()) {
         this.executeTask(schedule.get());
         return true;
      } else {
         LOG.warn(LogTypes.SERVER_INFO_SYS, "Cannot execute task \"" + taskName + "\" because task was not found.");
         return false;
      }
   }

   private Optional<TaskSchedule> findTaskScheduleByName(String taskName) {
      return this.tasks.stream().filter(t -> Strings.CS.equals(taskName, t.getTaskName())).findFirst();
   }

   @Override
   public String getName() {
      return "Task Scheduler";
   }

   @Override
   public String getThreadName() {
      return "TaskScheduler";
   }

   @Override
   public void pleaseStop() {
      super.pleaseStop();
      this.tasks.forEach(t -> t.getTask().pleaseStop());
   }

   @BeforePluginDeactivation
   void beforePluginDeactivation() {
      this.pleaseStop();
   }

   @Override
   public Class<TaskSchedulerServicePresenter> getPresenterClass() {
      return TaskSchedulerServicePresenter.class;
   }

   public Long getCheckIntervalMillis() {
      return this.checkIntervalMillis;
   }

   public List<TaskSchedule> getTasks() {
      return ImmutableList.copyOf(this.tasks);
   }

   @ConfigurationValidator
   static void validateConfiguration(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws ConfigurationException {
      Set<String> uniqueTaskNames = new HashSet<>();

      for (PluginConfiguration taskScheduleConfig : PluginListPropertyValueReader.effectivePluginListPropertyValue(config, "tasks", env, context)) {
         String taskName = StringPropertyValueReader.effectiveStringConfigValue(taskScheduleConfig, "name", env, context);
         boolean resultOfAddingNewTaskName = uniqueTaskNames.add(taskName);
         if (!resultOfAddingNewTaskName) {
            throw new PropertyConstraintViolationException(
               "tasks", config, env, context, "Task names are not unique. Task name \"" + taskName + "\" found multiple times."
            );
         }
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static TaskSchedulerService.TaskSchedulerServiceBuilder builder() {
      return new TaskSchedulerService.TaskSchedulerServiceBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public TaskSchedulerService() {
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class TaskSchedulerServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private List<TaskSchedule> tasks;
      @SuppressFBWarnings(justification = "generated code")
      private long checkIntervalMillis;
      @SuppressFBWarnings(justification = "generated code")
      private boolean runTasksConcurrently;

      @SuppressFBWarnings(justification = "generated code")
      TaskSchedulerServiceBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public TaskSchedulerService.TaskSchedulerServiceBuilder tasks(List<TaskSchedule> tasks) {
         this.tasks = tasks;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TaskSchedulerService.TaskSchedulerServiceBuilder checkIntervalMillis(long checkIntervalMillis) {
         this.checkIntervalMillis = checkIntervalMillis;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TaskSchedulerService.TaskSchedulerServiceBuilder runTasksConcurrently(boolean runTasksConcurrently) {
         this.runTasksConcurrently = runTasksConcurrently;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TaskSchedulerService build() {
         return new TaskSchedulerService(this.tasks, this.checkIntervalMillis, this.runTasksConcurrently);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "TaskSchedulerService.TaskSchedulerServiceBuilder(tasks="
            + this.tasks
            + ", checkIntervalMillis="
            + this.checkIntervalMillis
            + ", runTasksConcurrently="
            + this.runTasksConcurrently
            + ")";
      }
   }
}
