import type { Logger } from '@/util/logging/LoggerFactory.js';
import { LoggerFactory } from '@/util/logging/LoggerFactory.js';
import dayjs from 'dayjs';
import type { interfaces } from 'inversify';
import { isDate, isFunction, isString } from 'lodash-es';
import type { RecurrenceRule } from 'node-schedule';
import schedule from 'node-schedule';
import type z from 'zod';
import { TypedError } from '../types/errors.ts';
import { Result } from '../types/result.ts';
import type { Task, Task2, TaskConstructor } from './Task.js';

type ScheduleRule = RecurrenceRule | Date | string | number;

export type TaskFactoryFn<OutType, Args extends unknown[] = []> = () => Task<
  Args,
  OutType
>;

export type Task2FactoryFn<
  InputT extends z.ZodType,
  OutputT = void,
> = () => Task2<InputT, OutputT>;

type ScheduledTaskOptions = {
  visible?: boolean;
  runOnSchedule?: boolean;
  runAtStartup?: boolean;
};

export type UnknownScheduledTask = ScheduledTask<z.ZodType, unknown>;

export class ScheduledTask<
  // // TaskInputSchemaT extends z.ZodType = z.ZodUnknown,
  // // TaskOutputTypeT = unknown,
  // // TaskT extends Task2<TaskInputSchemaT, TaskOutputTypeT> = Task2<
  // //   TaskInputSchemaT,
  // //   TaskOutputTypeT
  // // >,
  // TaskT extends Task2<z.ZodUnknown, unknown>,
  // TaskInputSchemaT extends z.ZodType = TaskT extends Task2<infer In, unknown> ? In : never,
  // TaskOutputTypeT = TaskT extends Task2<z.ZodUnknown, infer Out> ? Out : never,
  TaskInputSchemaT extends z.ZodType,
  OutputT,
  // TaskT extends TaskConstructor<TaskInputSchemaT, OutputT>,
> {
  protected logger: Logger;
  protected scheduledJob: schedule.Job;

  private factory: interfaces.AutoFactory<Task2<TaskInputSchemaT, OutputT>>;
  public readonly schedule: ScheduleRule;

  public running: boolean = false;
  public runAtStartup = false;
  public visible: boolean = true;
  public lastExecution?: Date;

  constructor(
    jobConstructor: TaskConstructor<TaskInputSchemaT, OutputT> | string,
    scheduleRule: ScheduleRule,
    taskFactory: interfaces.AutoFactory<Task2<TaskInputSchemaT, OutputT>>,
    public readonly presetArgs:
      | z.infer<TaskInputSchemaT>
      | (() => z.infer<TaskInputSchemaT>),
    options?: ScheduledTaskOptions,
  ) {
    const jobName = isString(jobConstructor)
      ? jobConstructor
      : jobConstructor.name;
    this.logger = LoggerFactory.child({
      task: jobName,
      className: this.constructor.name,
      caller: import.meta,
    });
    this.schedule = scheduleRule;
    this.factory = taskFactory;
    this.scheduledJob = schedule.scheduleJob(jobName, scheduleRule, () =>
      this.run(),
    );

    this.visible = options?.visible ?? true;

    if (options?.runOnSchedule) {
      schedule.scheduleJob(jobName, dayjs().add(5, 'seconds').toDate(), () =>
        this.runJobInternal(),
      );
    }

    if (options?.runAtStartup) {
      this.runAtStartup = options.runAtStartup;
    }
  }

  get name() {
    return this.scheduledJob.name;
  }

  // ScheduledTasks overrides can reimplement this.
  protected run() {
    return this.runJobInternal();
  }

  // Runs an instance of this task now, cancels the next invocation
  // and reschedules the job on the original schedule.
  // If background=true, this function will not return the underlying
  // Promise generated by the running job and all errors will be swallowed.
  async runNow(background: boolean = true): Promise<OutputT> {
    this.scheduledJob.cancelNext(false);
    // Can't reschedule a one-off job
    const rescheduleCb = () =>
      isDate(this.schedule)
        ? void 0
        : this.scheduledJob.reschedule(this.schedule);
    if (background) {
      return new Promise<OutputT>((resolve, reject) => {
        this.runJobInternal()
          .then((result) => {
            if (result.isFailure()) {
              reject(result.error);
              return;
            }
            resolve(result.get());
          })
          .catch(reject)
          .finally(rescheduleCb);
      });
    } else {
      const result = await this.runJobInternal();
      rescheduleCb();
      return result.getOrThrow();
    }
  }

  cancel(reschedule: boolean = false) {
    this.scheduledJob.cancel(reschedule);
  }

  removeFromSchedule() {
    this.cancel(false);
  }

  nextExecution() {
    return this.scheduledJob.nextInvocation();
  }

  protected async runJobInternal(): Promise<Result<OutputT>> {
    this.running = true;
    const instance = this.factory();
    try {
      return await instance.run(
        isFunction(this.presetArgs) ? this.presetArgs() : this.presetArgs,
      );
    } catch (e) {
      this.logger.error(e, 'Error while running job: %s', instance.taskName);
      return Result.forError(TypedError.fromAny(e));
    } finally {
      this.running = false;
      this.lastExecution = new Date();
    }
  }
}
