/**
 * Minimal 5-field cron "next run" calculator, shared by the event scheduler
 * (SchedulesService) and the wipe scheduler (WipesService).
 *
 * Supports `*` and exact numeric fields (minute hour day-of-month month
 * day-of-week). Walks minute-by-minute up to 366 days ahead. Returns null on a
 * malformed expression or if no match is found within a year.
 *
 * NOTE: the expression is evaluated in **UTC**. A per-schedule `timezone`
 * column exists on both schedule tables but is NOT yet honored here — fixing it
 * properly needs a timezone-aware cron library; tracked as a shared follow-up.
 */
export function nextCronDate(expr: string, after: Date): Date | null {
  const parts = expr.trim().split(/\s+/);
  if (parts.length !== 5) return null;

  const [minuteExpr, hourExpr, domExpr, monthExpr, dowExpr] = parts;

  const matches = (e: string, value: number): boolean => {
    if (e === '*') return true;
    return parseInt(e, 10) === value;
  };

  // Walk minute-by-minute up to 366 days forward to find the next match.
  const candidate = new Date(after.getTime() + 60_000); // advance at least 1 minute
  candidate.setSeconds(0, 0);

  const limit = new Date(after.getTime() + 366 * 24 * 60 * 60 * 1000);

  while (candidate < limit) {
    const min = candidate.getUTCMinutes();
    const hour = candidate.getUTCHours();
    const dom = candidate.getUTCDate();
    const month = candidate.getUTCMonth() + 1; // 1-12
    const dow = candidate.getUTCDay(); // 0=Sun

    if (
      matches(minuteExpr, min) &&
      matches(hourExpr, hour) &&
      matches(domExpr, dom) &&
      matches(monthExpr, month) &&
      matches(dowExpr, dow)
    ) {
      return candidate;
    }

    candidate.setTime(candidate.getTime() + 60_000);
  }

  return null;
}
