namespace ClaudeDo.Worker.Prime;

public sealed record NextDue(PrimeScheduleDto Schedule, DateTimeOffset At, bool FireImmediately);

public static class NextDueCalculator
{
    public static NextDue? Compute(
        IEnumerable<PrimeScheduleDto> 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 (s.EndDate < DateOnly.FromDateTime(now.LocalDateTime)) return null;

        var todayLocal = DateOnly.FromDateTime(now.LocalDateTime);
        var alreadyFiredToday = s.LastRunAt is { } last &&
                                DateOnly.FromDateTime(last.LocalDateTime) == todayLocal;

        if (!alreadyFiredToday)
        {
            var startOrToday = s.StartDate > todayLocal ? s.StartDate : todayLocal;
            if (startOrToday == todayLocal && 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);
        if (s.StartDate > d) d = s.StartDate;
        for (int i = 0; i < 8; i++)
        {
            if (d > s.EndDate) return null;
            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)
    {
        if (d < s.StartDate || d > s.EndDate) return false;
        if (!s.WorkdaysOnly) return true;
        var dow = d.ToDateTime(TimeOnly.MinValue).DayOfWeek;
        return dow != DayOfWeek.Saturday && dow != DayOfWeek.Sunday;
    }

    private static DateTimeOffset ToOffset(DateOnly day, TimeSpan time, TimeSpan offset) =>
        new(day.Year, day.Month, day.Day, time.Hours, time.Minutes, time.Seconds, offset);
}
