import { Cron } from "croner";
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { jobAuditLog } from "../db/schema/auditLog.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import type { ReturnHelper } from "./returnHelper.utils.js";

// example createJob
// createCronJob("test Cron", "*/5 * * * * *", async () => {
// 	console.log("help");
// });

export interface JobInfo {
	name: string;
	schedule: string;
	nextRun: Date | null;
	isRunning: boolean;
}

// Store running cronjobs
export const runningCrons: Record<string, Cron> = {};
const activeRuns = new Set<string>();
const log = createLogger({ module: "system", subModule: "croner" });
const cronStats: Record<string, { created: number; replaced: number }> = {};

// how to se the times
//  * ┌──────────────── (optional) second (0 - 59)
//  * │ ┌────────────── minute (0 - 59)
//  * │ │ ┌──────────── hour (0 - 23)
//  * │ │ │ ┌────────── day of month (1 - 31)
//  * │ │ │ │ ┌──────── month (1 - 12, JAN-DEC)
//  * │ │ │ │ │ ┌────── day of week (0 - 6, SUN-Mon)
//  * │ │ │ │ │ │       (0 to 6 are Sunday to Saturday; 7 is Sunday, the same as 0)
//  * │ │ │ │ │ │ ┌──── (optional) year (1 - 9999)
//  * │ │ │ │ │ │ │
//  * * 05 * * * * *
// note that when leaving * anywhere means ever part of that. IE * in the seconds part will run every second inside that min

/**
 *
 * @param name Name of the job we want to run
 * @param schedule Cron expression (example: `*\/5 * * * * *`)
 * @param task Async function that will run
 * @param source we can add where it came from to assist in getting this tracked down, more for debugging
 */
export const createCronJob = async (
	name: string,
	schedule: string, // cron string with 8 8 IE: */5 * * * * * every 5th second
	task: () => Promise<void | ReturnHelper>, // what function are we passing over
	source = "unknown",
) => {
	// get the timezone based on the os timezone set
	const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

	// initial go so just store it this is more for debugging to see if something crazy keeps happening
	if (!cronStats[name]) {
		cronStats[name] = { created: 0, replaced: 0 };
	}

	// Destroy existing job if it exist
	if (runningCrons[name]) {
		cronStats[name].replaced += 1;
		log.warn(
			{
				job: name,
				source,
				oldSchedule: runningCrons[name].getPattern?.(),
				newSchedule: schedule,
				replaceCount: cronStats[name].replaced,
			},
			`Cron job "${name}" already existed and is being replaced`,
		);

		runningCrons[name].stop();
	}

	// Create new job with Croner
	runningCrons[name] = new Cron(
		schedule,
		{
			timezone: timeZone,
			catch: true, // Prevents unhandled rejections
			name: name,
		},
		async () => {
			if (activeRuns.has(name)) {
				log.warn({ jobName: name }, "Skipping overlapping cron execution");
				return;
			}

			activeRuns.add(name);

			const startedAt = new Date();
			const start = Date.now();

			let executionId: string = "";

			try {
				const [execution] = await db
					.insert(jobAuditLog)
					.values({
						jobName: name,
						startedAt,
						status: "running",
					})
					.returning();

				executionId = execution?.id as string;

				await task?.();

				// tell it we done
				await db
					.update(jobAuditLog)
					.set({
						finishedAt: new Date(),
						durationMs: Date.now() - start,
						status: "success",
					})
					.where(eq(jobAuditLog.id, executionId));
			} catch (e: any) {
				if (executionId) {
					await db
						.update(jobAuditLog)
						.set({
							finishedAt: new Date(),
							durationMs: Date.now() - start,
							status: "error",
							errorMessage: e.message,
							errorStack: e.stack,
						})
						.where(eq(jobAuditLog.id, executionId));
				}
			} finally {
				activeRuns.delete(name);
			}
		},
	);

	log.info({}, `A job for ${name} was just created.`);
};

export const getAllJobs = (): JobInfo[] => {
	return Object.entries(runningCrons).map(([name, job]) => ({
		name,
		schedule: job.getPattern() || "invalid",
		nextRun: job.nextRun() || null,
		lastRun: job.previousRun() || null,
		isRunning: job.isRunning(), //job ? !job.isStopped() : false,
	}));
};

export const removeCronJob = (name: string) => {
	if (runningCrons[name]) {
		runningCrons[name].stop();
		delete runningCrons[name];
	}
};

export const stopCronJob = (name: string) => {
	if (runningCrons[name]) {
		runningCrons[name].pause();
		log.info(
			{},
			`${name} was just stopped either manually or due to a setting change.`,
		);
	}
};

export const resumeCronJob = (name: string) => {
	if (runningCrons[name]) {
		runningCrons[name].resume();
		log.info(
			{},
			`${name} was just restarted either manually or due to a setting change.`,
		);
	}
};
