import {IJobManager} from '../database/interfaces/IJobManager';
import {JobProgressDTO, JobProgressStates} from '../../../common/entities/job/JobProgressDTO';
import {IJob} from './jobs/IJob';
import {JobRepository} from './JobRepository';
import {Config} from '../../../common/config/private/Config';
import {AfterJobTrigger, JobScheduleDTO, JobTriggerType} from '../../../common/entities/job/JobScheduleDTO';
import {Logger} from '../../Logger';
import {NotificationManager} from '../NotifocationManager';
import {IJobListener} from './jobs/IJobListener';
import {JobProgress} from './jobs/JobProgress';
import {JobProgressManager} from './JobProgressManager';


const LOG_TAG = '[JobManager]';

export class JobManager implements IJobManager, IJobListener {
  protected timers: { schedule: JobScheduleDTO, timer: NodeJS.Timeout }[] = [];
  protected progressManager: JobProgressManager = null;

  constructor() {
    this.progressManager = new JobProgressManager();
    this.runSchedules();
  }

  protected get JobRunning(): boolean {
    return JobRepository.Instance.getAvailableJobs().findIndex(j => j.InProgress === true) !== -1;
  }

  protected get JobNoParallelRunning(): boolean {
    return JobRepository.Instance.getAvailableJobs()
      .findIndex(j => j.InProgress === true && j.allowParallelRun) !== -1;
  }

  getProgresses(): { [id: string]: JobProgressDTO } {
    return this.progressManager.Progresses;
  }

  async run<T>(jobName: string, config: T, soloRun: boolean, allowParallelRun: boolean): Promise<void> {
    if ((allowParallelRun === false && this.JobRunning === true) || this.JobNoParallelRunning === true) {
      throw new Error('Can\'t start this job while an other is running');
    }

    const t = this.findJob(jobName);
    if (t) {
      t.JobListener = this;
      await t.start(config, soloRun, allowParallelRun);
    } else {
      Logger.warn(LOG_TAG, 'cannot find job to start:' + jobName);
    }
  }

  stop(jobName: string): void {
    const t = this.findJob(jobName);
    if (t) {
      t.cancel();
    } else {
      Logger.warn(LOG_TAG, 'cannot find job to stop:' + jobName);
    }
  }

  onProgressUpdate = (progress: JobProgress): void => {
    this.progressManager.onJobProgressUpdate(progress.toDTO());
  };

  onJobFinished = async (job: IJob<any>, state: JobProgressStates, soloRun: boolean): Promise<void> => {
    // if it was not finished peacefully or was a soloRun, do not start the next one
    if (state !== JobProgressStates.finished || soloRun === true) {
      return;
    }
    const sch = Config.Server.Jobs.scheduled.find(s => s.jobName === job.Name);
    if (sch) {
      const children = Config.Server.Jobs.scheduled.filter(s => s.trigger.type === JobTriggerType.after &&
        (<AfterJobTrigger>s.trigger).afterScheduleName === sch.name);
      for (let i = 0; i < children.length; ++i) {
        try {
          await this.run(children[i].jobName, children[i].config, false, children[i].allowParallelRun);
        } catch (e) {
          NotificationManager.warning('Job running error:' + children[i].name, e.toString());
        }
      }
    }
  };

  getAvailableJobs(): IJob<any>[] {
    return JobRepository.Instance.getAvailableJobs();
  }

  public stopSchedules(): void {
    this.timers.forEach(t => clearTimeout(t.timer));
    this.timers = [];
  }

  public runSchedules(): void {
    this.stopSchedules();
    Logger.info(LOG_TAG, 'Running job schedules');
    Config.Server.Jobs.scheduled.forEach(s => this.runSchedule(s));
  }

  protected findJob<T = any>(jobName: string): IJob<T> {
    return this.getAvailableJobs().find(t => t.Name === jobName);
  }

  private runSchedule(schedule: JobScheduleDTO) {
    const nextDate = JobScheduleDTO.getNextRunningDate(new Date(), schedule);
    if (nextDate && nextDate.getTime() > Date.now()) {
      Logger.debug(LOG_TAG, 'running schedule: ' + schedule.jobName +
        ' at ' + nextDate.toLocaleString(undefined, {hour12: false}));

      const timer: NodeJS.Timeout = setTimeout(async () => {
        this.timers = this.timers.filter(t => t.timer !== timer);
        await this.run(schedule.jobName, schedule.config, false, schedule.allowParallelRun);
        this.runSchedule(schedule);
      }, nextDate.getTime() - Date.now());
      this.timers.push({schedule: schedule, timer: timer});

    } else {
      Logger.debug(LOG_TAG, 'skipping schedule:' + schedule.jobName);
    }
  }

}
