package core

import (
	"time"
)

// NextCronTime returns the next time.Time that satisfies schedule after the given time.
// It advances minute by minute, skipping ahead when a field does not match.
// Returns the zero value of time.Time if no match is found within 366 days (impossible schedule).
func NextCronTime(schedule CronSchedule, after time.Time) time.Time {
	// Truncate to minute, then advance by 1 minute.
	t := after.Truncate(time.Minute).Add(time.Minute)

	limit := after.Add(366 * 24 * time.Hour)

	for t.Before(limit) {
		// Check month (1-12).
		if !intIn(int(t.Month()), schedule.Month) {
			// Advance to first day of next valid month.
			t = nextValidMonth(t, schedule.Month)
			if t.IsZero() {
				return time.Time{}
			}
			continue
		}

		// Check day of month AND day of week (cron uses OR semantics when both are restricted,
		// but standard 5-field cron: if both are non-wildcard, either can match).
		// For simplicity we use AND semantics (both must match) which is the POSIX default
		// for the common case; most implementations differ only when both are explicitly set.
		domOK := intIn(t.Day(), schedule.DayOfMonth)
		dowOK := intIn(int(t.Weekday()), schedule.DayOfWeek)
		if !domOK || !dowOK {
			// Advance to next day at midnight.
			t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location())
			continue
		}

		// Check hour.
		if !intIn(t.Hour(), schedule.Hour) {
			// Advance to next valid hour.
			next := nextValidHour(t, schedule.Hour)
			if next.IsZero() {
				// No valid hour today; advance to tomorrow.
				t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location())
			} else {
				t = next
			}
			continue
		}

		// Check minute.
		if !intIn(t.Minute(), schedule.Minute) {
			next := nextValidMinute(t, schedule.Minute)
			if next.IsZero() {
				// No more valid minutes this hour; advance to next hour.
				t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour()+1, 0, 0, 0, t.Location())
			} else {
				t = next
			}
			continue
		}

		// All fields match.
		return t
	}

	return time.Time{}
}

// intIn returns true if v is in the sorted slice s.
func intIn(v int, s []int) bool {
	for _, x := range s {
		if x == v {
			return true
		}
	}
	return false
}

// nextValidMonth advances t to the first moment of the next valid month.
func nextValidMonth(t time.Time, months []int) time.Time {
	month := int(t.Month())
	for _, m := range months {
		if m > month {
			return time.Date(t.Year(), time.Month(m), 1, 0, 0, 0, 0, t.Location())
		}
	}
	// Wrap to next year.
	if len(months) > 0 {
		return time.Date(t.Year()+1, time.Month(months[0]), 1, 0, 0, 0, 0, t.Location())
	}
	return time.Time{}
}

// nextValidHour returns t at the next valid hour this day, or zero if none.
func nextValidHour(t time.Time, hours []int) time.Time {
	h := t.Hour()
	for _, hh := range hours {
		if hh > h {
			return time.Date(t.Year(), t.Month(), t.Day(), hh, 0, 0, 0, t.Location())
		}
	}
	return time.Time{}
}

// nextValidMinute returns t at the next valid minute this hour, or zero if none.
func nextValidMinute(t time.Time, minutes []int) time.Time {
	m := t.Minute()
	for _, mm := range minutes {
		if mm > m {
			return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), mm, 0, 0, t.Location())
		}
	}
	return time.Time{}
}
