// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { core, internals, primordials } from "ext:core/mod.js";
const {
  isPromise,
} = core;
import { op_cron_create, op_cron_next } from "ext:core/ops";
const {
  ArrayPrototypeJoin,
  NumberPrototypeToString,
  TypeError,
} = primordials;

export function formatToCronSchedule(
  value?: number | { exact: number | number[] } | {
    start?: number;
    end?: number;
    every?: number;
  },
): string {
  if (value === undefined) {
    return "*";
  } else if (typeof value === "number") {
    return NumberPrototypeToString(value);
  } else {
    const { exact } = value as { exact: number | number[] };
    if (exact === undefined) {
      const { start, end, every } = value as {
        start?: number;
        end?: number;
        every?: number;
      };
      if (start !== undefined && end !== undefined && every !== undefined) {
        return start + "-" + end + "/" + every;
      } else if (start !== undefined && end !== undefined) {
        return start + "-" + end;
      } else if (start !== undefined && every !== undefined) {
        return start + "/" + every;
      } else if (start !== undefined) {
        return start + "/1";
      } else if (end === undefined && every !== undefined) {
        return "*/" + every;
      } else {
        throw new TypeError(
          `Invalid cron schedule: start=${start}, end=${end}, every=${every}`,
        );
      }
    } else {
      if (typeof exact === "number") {
        return NumberPrototypeToString(exact);
      } else {
        return ArrayPrototypeJoin(exact, ",");
      }
    }
  }
}

export function parseScheduleToString(
  schedule: string | Deno.CronSchedule,
): string {
  if (typeof schedule === "string") {
    return schedule;
  } else {
    let {
      minute,
      hour,
      dayOfMonth,
      month,
      dayOfWeek,
    } = schedule;

    // Automatically override unspecified values for convenience. For example,
    // to run every 2 hours, `{ hour: { every: 2 } }` can be specified without
    // explicitly specifying `minute`.
    if (minute !== undefined) {
      // Nothing to override.
    } else if (hour !== undefined) {
      // Override minute to 0 since it's not specified.
      minute = 0;
    } else if (dayOfMonth !== undefined || dayOfWeek !== undefined) {
      // Override minute and hour to 0 since they're not specified.
      minute = 0;
      hour = 0;
    } else if (month !== undefined) {
      // Override minute and hour to 0, and dayOfMonth to 1 since they're not specified.
      minute = 0;
      hour = 0;
      dayOfMonth = 1;
    }

    return formatToCronSchedule(minute) +
      " " + formatToCronSchedule(hour) +
      " " + formatToCronSchedule(dayOfMonth) +
      " " + formatToCronSchedule(month) +
      " " + formatToCronSchedule(dayOfWeek);
  }
}

function cron(
  name: string,
  schedule: string | Deno.CronSchedule,
  handlerOrOptions1:
    | (() => Promise<void> | void)
    | ({ backoffSchedule?: number[]; signal?: AbortSignal }),
  handler2?: () => Promise<void> | void,
) {
  if (name === undefined) {
    throw new TypeError(
      "Cannot create cron job, a unique name is required: received 'undefined'",
    );
  }
  if (schedule === undefined) {
    throw new TypeError(
      "Cannot create cron job, a schedule is required: received 'undefined'",
    );
  }

  schedule = parseScheduleToString(schedule);

  let handler: () => Promise<void> | void;
  let options:
    | { backoffSchedule?: number[]; signal?: AbortSignal }
    | undefined = undefined;

  if (typeof handlerOrOptions1 === "function") {
    handler = handlerOrOptions1;
    if (handler2 !== undefined) {
      throw new TypeError(
        "Cannot create cron job, a single handler is required: two handlers were specified",
      );
    }
  } else if (typeof handler2 === "function") {
    handler = handler2;
    options = handlerOrOptions1;
  } else {
    throw new TypeError("Cannot create cron job: a handler is required");
  }

  const rid = op_cron_create(
    name,
    schedule,
    options?.backoffSchedule,
  );

  if (options?.signal) {
    const signal = options?.signal;
    signal.addEventListener(
      "abort",
      () => {
        core.close(rid);
      },
      { once: true },
    );
  }

  return (async () => {
    let success = true;
    while (true) {
      const r = await op_cron_next(rid, success);
      if (r === false) {
        break;
      }
      try {
        const result = handler();
        const _res = isPromise(result) ? (await result) : result;
        success = true;
      } catch (error) {
        // deno-lint-ignore no-console
        console.error(`Exception in cron handler ${name}`, error);
        success = false;
      }
    }
  })();
}

// For testing
internals.formatToCronSchedule = formatToCronSchedule;
internals.parseScheduleToString = parseScheduleToString;

export { cron };