using ClaudeDo.Data.Models; namespace ClaudeDo.Worker.Prime; public sealed record NextDue(PrimeScheduleDto Schedule, DateTimeOffset At, bool FireImmediately); public static class NextDueCalculator { public static NextDue? Compute( IEnumerable schedules, DateTimeOffset now, TimeSpan catchUp) { NextDue? best = null; foreach (var s in schedules) { if (!s.Enabled) continue; var due = ComputeFor(s, now, catchUp); if (due is null) continue; if (best is null || due.At < best.At) best = due; } return best; } private static NextDue? ComputeFor(PrimeScheduleDto s, DateTimeOffset now, TimeSpan catchUp) { if ((PrimeDays)s.Days == PrimeDays.None) return null; var todayLocal = DateOnly.FromDateTime(now.LocalDateTime); var alreadyFiredToday = s.LastRunAt is { } last && DateOnly.FromDateTime(last.LocalDateTime) == todayLocal; if (!alreadyFiredToday && IsEligibleDay(s, todayLocal)) { var todayTarget = ToOffset(todayLocal, s.TimeOfDay, now.Offset); if (todayTarget >= now) return new NextDue(s, todayTarget, false); if (now <= todayTarget + catchUp) return new NextDue(s, now, true); } var d = todayLocal.AddDays(1); for (int i = 0; i < 7; i++) { if (IsEligibleDay(s, d)) return new NextDue(s, ToOffset(d, s.TimeOfDay, now.Offset), false); d = d.AddDays(1); } return null; } private static bool IsEligibleDay(PrimeScheduleDto s, DateOnly d) => ((PrimeDays)s.Days & ToFlag(d.DayOfWeek)) != PrimeDays.None; private static PrimeDays ToFlag(DayOfWeek dow) => dow switch { DayOfWeek.Monday => PrimeDays.Monday, DayOfWeek.Tuesday => PrimeDays.Tuesday, DayOfWeek.Wednesday => PrimeDays.Wednesday, DayOfWeek.Thursday => PrimeDays.Thursday, DayOfWeek.Friday => PrimeDays.Friday, DayOfWeek.Saturday => PrimeDays.Saturday, DayOfWeek.Sunday => PrimeDays.Sunday, _ => PrimeDays.None, }; private static DateTimeOffset ToOffset(DateOnly day, TimeSpan time, TimeSpan offset) => new(day.Year, day.Month, day.Day, time.Hours, time.Minutes, time.Seconds, offset); }