import type { ScheduledTask } from "node-cron";

import { objectEntries, objectKeys } from "@homarr/common";
import { db } from "@homarr/db";

import type { JobCallback } from "./creator";
import type { Logger } from "./logger";
import { jobRegistry } from "./registry";

type Jobs<TAllowedNames extends string> = {
  [name in TAllowedNames]: ReturnType<JobCallback<TAllowedNames, name>>;
};

export interface CreateCronJobGroupCreatorOptions {
  logger: Logger;
}

export const createJobGroupCreator = <TAllowedNames extends string = string>(
  options: CreateCronJobGroupCreatorOptions,
) => {
  return <TJobs extends Jobs<TAllowedNames>>(jobs: TJobs) => {
    options.logger.logDebug(`Creating job group with ${Object.keys(jobs).length} jobs.`);
    for (const [key, job] of objectEntries(jobs)) {
      if (typeof key !== "string" || typeof job.name !== "string") continue;

      options.logger.logDebug(`Added job ${job.name} to the job registry.`);
      jobRegistry.set(key, {
        ...job,
        name: job.name,
      });
    }

    const tasks = new Map<string, ScheduledTask>();

    return {
      initializeAsync: async () => {
        const configurations = await db.query.cronJobConfigurations.findMany();
        for (const job of jobRegistry.values()) {
          const configuration = configurations.find(({ name }) => name === job.name);
          if (configuration?.isEnabled === false) {
            continue;
          }
          if (tasks.has(job.name)) {
            continue;
          }

          const scheduledTask = await job.createTaskAsync();
          if (!scheduledTask) continue;

          tasks.set(job.name, scheduledTask);
        }
      },
      startAsync: async (name: keyof TJobs) => {
        const job = jobRegistry.get(name as string);
        if (!job) return;
        if (!tasks.has(job.name)) return;

        options.logger.logInfo(`Starting schedule cron job ${job.name}.`);
        await job.onStartAsync();
        await tasks.get(name as string)?.start();
      },
      startAllAsync: async () => {
        for (const job of jobRegistry.values()) {
          if (!tasks.has(job.name)) {
            continue;
          }

          options.logger.logInfo(`Starting schedule of cron job ${job.name}.`);
          await job.onStartAsync();
          await tasks.get(job.name)?.start();
        }
      },
      runManuallyAsync: async (name: keyof TJobs) => {
        const job = jobRegistry.get(name as string);
        if (!job) return;
        if (job.preventManualExecution) {
          throw new Error(`The job "${job.name}" can not be executed manually.`);
        }

        options.logger.logInfo(`Running schedule cron job ${job.name} manually.`);
        await tasks.get(name as string)?.execute();
      },
      stopAsync: async (name: keyof TJobs) => {
        const job = jobRegistry.get(name as string);
        if (!job) return;

        options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
        await tasks.get(name as string)?.stop();
      },
      stopAllAsync: async () => {
        for (const job of jobRegistry.values()) {
          options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
          await tasks.get(job.name)?.stop();
        }
      },
      getJobRegistry() {
        return jobRegistry as Map<TAllowedNames, ReturnType<JobCallback<TAllowedNames, TAllowedNames>>>;
      },
      getTask(name: keyof TJobs) {
        return tasks.get(name as string) ?? null;
      },
      setTask(name: keyof TJobs, task: ScheduledTask) {
        tasks.set(name as string, task);
      },
      getKeys() {
        return objectKeys(jobs);
      },
    };
  };
};
