package scheduler

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"
)

// Cron implements the standard five-field cron format: minute, hour,
// day-of-month, month, day-of-week. Lists, ranges and steps are supported.
type Cron struct {
	minute, hour, day, month, weekday field
	dayWildcard, weekdayWildcard      bool
}

type field struct {
	min, max int
	values   map[int]bool
}

func ParseCron(expression string) (Cron, error) {
	parts := strings.Fields(expression)
	if len(parts) != 5 {
		return Cron{}, errors.New("cron 表达式必须包含 5 段: 分 时 日 月 周")
	}
	definitions := [5][2]int{{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 7}}
	fields := make([]field, 5)
	for i, part := range parts {
		parsed, err := parseField(part, definitions[i][0], definitions[i][1], i == 4)
		if err != nil {
			return Cron{}, fmt.Errorf("cron 第 %d 段无效: %w", i+1, err)
		}
		fields[i] = parsed
	}
	return Cron{minute: fields[0], hour: fields[1], day: fields[2], month: fields[3], weekday: fields[4], dayWildcard: parts[2] == "*", weekdayWildcard: parts[4] == "*"}, nil
}

func parseField(raw string, minimum, maximum int, weekday bool) (field, error) {
	result := field{min: minimum, max: maximum, values: map[int]bool{}}
	for _, item := range strings.Split(raw, ",") {
		item = strings.TrimSpace(item)
		if item == "" {
			return field{}, errors.New("存在空列表项")
		}
		base, stepText, hasStep := strings.Cut(item, "/")
		step := 1
		if hasStep {
			var err error
			step, err = strconv.Atoi(stepText)
			if err != nil || step < 1 || step > maximum-minimum+1 {
				return field{}, errors.New("步长无效")
			}
		}
		start, end := minimum, maximum
		switch {
		case base == "*":
		case strings.Contains(base, "-"):
			left, right, _ := strings.Cut(base, "-")
			var err error
			start, err = cronNumber(left, minimum, maximum, weekday)
			if err != nil {
				return field{}, err
			}
			end, err = cronNumber(right, minimum, maximum, weekday)
			if err != nil || start > end {
				return field{}, errors.New("范围无效")
			}
		default:
			var err error
			start, err = cronNumber(base, minimum, maximum, weekday)
			if err != nil {
				return field{}, err
			}
			end = start
			if hasStep {
				end = maximum
			}
		}
		for value := start; value <= end; value += step {
			if weekday && value == 7 {
				value = 0
				result.values[value] = true
				break
			}
			result.values[value] = true
		}
	}
	if len(result.values) == 0 {
		return field{}, errors.New("没有可用取值")
	}
	return result, nil
}

func cronNumber(raw string, minimum, maximum int, weekday bool) (int, error) {
	value, err := strconv.Atoi(raw)
	if err != nil || value < minimum || value > maximum {
		return 0, fmt.Errorf("%q 超出 %d-%d", raw, minimum, maximum)
	}
	if weekday && value == 7 {
		return 7, nil
	}
	return value, nil
}

func (c Cron) Matches(value time.Time) bool {
	dayMatch := c.day.values[value.Day()]
	weekdayMatch := c.weekday.values[int(value.Weekday())]
	calendarMatch := dayMatch && weekdayMatch
	// Vixie cron semantics: when both day fields are restricted, either may match.
	if !c.dayWildcard && !c.weekdayWildcard {
		calendarMatch = dayMatch || weekdayMatch
	}
	return c.minute.values[value.Minute()] && c.hour.values[value.Hour()] && c.month.values[int(value.Month())] && calendarMatch
}

func (c Cron) Next(after time.Time, location *time.Location) (time.Time, error) {
	if location == nil {
		location = time.UTC
	}
	candidate := after.UTC().Truncate(time.Minute).Add(time.Minute)
	deadline := candidate.AddDate(5, 0, 0)
	for candidate.Before(deadline) {
		if c.Matches(candidate.In(location)) {
			return candidate, nil
		}
		candidate = candidate.Add(time.Minute)
	}
	return time.Time{}, errors.New("未来 5 年内无匹配执行时间")
}
