// Pure schedule arithmetic, shared by the scheduler and the settings page.
//
// A schedule is "every N hours, anchored at HH:mm in an IANA timezone". Each
// occurrence is a *slot*; the scheduler runs each slot at most once. Daily is
// simply N = 24 — other frequencies need no new code here.

export type Frequency = "HOURLY" | "EVERY_6_HOURS" | "EVERY_12_HOURS" | "DAILY";

export type Schedule = {
  frequency: Frequency;
  checkTime: string; // "HH:mm"
  timezone: string;
};

export const FREQUENCY_HOURS: Record<Frequency, number> = {
  HOURLY: 1,
  EVERY_6_HOURS: 6,
  EVERY_12_HOURS: 12,
  DAILY: 24,
};

export const FREQUENCY_LABELS: Record<Frequency, string> = {
  HOURLY: "Every 1 hour",
  EVERY_6_HOURS: "Every 6 hours",
  EVERY_12_HOURS: "Every 12 hours",
  DAILY: "Daily",
};

export const CHECK_TIME_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/;

export function isValidTimezone(timezone: string): boolean {
  try {
    new Intl.DateTimeFormat("en-US", { timeZone: timezone });
    return true;
  } catch {
    return false;
  }
}

type LocalDateTime = { year: number; month: number; day: number; hour: number; minute: number };

function localParts(instant: Date, timezone: string): LocalDateTime {
  const parts = new Intl.DateTimeFormat("en-US", {
    timeZone: timezone,
    hourCycle: "h23",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
  }).formatToParts(instant);
  const get = (type: Intl.DateTimeFormatPartTypes) => Number(parts.find((part) => part.type === type)?.value);
  return { year: get("year"), month: get("month"), day: get("day"), hour: get("hour"), minute: get("minute") };
}

/** The instant at which the wall clock in `timezone` reads `local`. */
function zonedTimeToUtc(local: LocalDateTime, timezone: string): Date {
  const wallClock = Date.UTC(local.year, local.month - 1, local.day, local.hour, local.minute);
  // Offset = how far the zone's wall clock is ahead of UTC at a given instant.
  const offsetAt = (instant: number) => {
    const p = localParts(new Date(instant), timezone);
    return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute) - instant;
  };
  // Two passes so the offset is taken at the result, not at the guess (matters around DST changes).
  const guess = wallClock - offsetAt(wallClock);
  return new Date(wallClock - offsetAt(guess));
}

/** Every slot on the local calendar day `dayOffset` days from `now`'s local date. */
function slotsOnDay(schedule: Schedule, now: Date, dayOffset: number): Date[] {
  const [hour, minute] = schedule.checkTime.split(":").map(Number);
  const intervalMinutes = FREQUENCY_HOURS[schedule.frequency] * 60;
  const today = localParts(now, schedule.timezone);

  const slots: Date[] = [];
  for (let k = 0; k < (24 * 60) / intervalMinutes; k++) {
    const minuteOfDay = (hour * 60 + minute + k * intervalMinutes) % (24 * 60);
    slots.push(
      zonedTimeToUtc(
        // Date.UTC normalises day overflow (e.g. day 0 or 32), so ±1 day is safe.
        { ...today, day: today.day + dayOffset, hour: Math.floor(minuteOfDay / 60), minute: minuteOfDay % 60 },
        schedule.timezone,
      ),
    );
  }
  return slots;
}

function nearbySlots(schedule: Schedule, now: Date): Date[] {
  return [-1, 0, 1].flatMap((dayOffset) => slotsOnDay(schedule, now, dayOffset)).sort((a, b) => a.getTime() - b.getTime());
}

/** The latest slot that is not in the future. */
export function mostRecentSlot(schedule: Schedule, now: Date): Date {
  const past = nearbySlots(schedule, now).filter((slot) => slot.getTime() <= now.getTime());
  return past[past.length - 1];
}

/** The first slot strictly after `now`. */
export function nextSlot(schedule: Schedule, now: Date): Date {
  return nearbySlots(schedule, now).find((slot) => slot.getTime() > now.getTime())!;
}

/**
 * The slot the scheduler should run now, or null. A slot is due once its time
 * has passed, however late (so a run missed during downtime is caught up),
 * but only if it falls after `notBefore` — the moment the schedule was last
 * saved — so editing the schedule never fires an immediate run.
 */
export function dueSlot(schedule: Schedule, now: Date, notBefore: Date): Date | null {
  const slot = mostRecentSlot(schedule, now);
  return slot.getTime() > notBefore.getTime() ? slot : null;
}
