package schedule

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

const maxCronSearchMinutes = 10 * 366 * 24 * 60

func isOneShotSource(sourceType string) bool {
	switch trim(sourceType) {
	case SourceTypeOneShotAbsolute, SourceTypeOneShotRelative:
		return true
	default:
		return false
	}
}

func initialOccurrenceForDefinition(definition Definition, now time.Time) (*time.Time, error) {
	now = utcOrNow(now)
	switch definition.SourceType {
	case SourceTypeOneShotAbsolute:
		if definition.ScheduledAt == nil {
			return nil, fmt.Errorf("one-shot absolute schedule requires scheduledAt")
		}
		occurrence := definition.ScheduledAt.UTC()
		return &occurrence, nil
	case SourceTypeOneShotRelative:
		if definition.ScheduledAt != nil {
			occurrence := definition.ScheduledAt.UTC()
			return &occurrence, nil
		}
		if definition.RelativeSeconds == nil || *definition.RelativeSeconds <= 0 {
			return nil, fmt.Errorf("one-shot relative schedule requires positive relativeSeconds")
		}
		occurrence := now.Add(time.Duration(*definition.RelativeSeconds) * time.Second).UTC()
		return &occurrence, nil
	case SourceTypeInterval:
		if definition.AnchorAt == nil || definition.IntervalSeconds == nil {
			return nil, fmt.Errorf("interval schedule requires anchorAt and intervalSeconds")
		}
		occurrence, err := nextIntervalOccurrence(*definition.AnchorAt, *definition.IntervalSeconds, now.Add(-time.Nanosecond))
		if err != nil {
			return nil, err
		}
		return &occurrence, nil
	case SourceTypeCron:
		occurrence, err := nextCronOccurrenceAfter(definition.CronExpression, definition.Timezone, now)
		if err != nil {
			return nil, err
		}
		return &occurrence, nil
	default:
		return nil, fmt.Errorf("unsupported schedule source type %q", definition.SourceType)
	}
}

func nextOccurrenceAfterDefinition(definition Definition, after time.Time) (*time.Time, error) {
	switch definition.SourceType {
	case SourceTypeInterval:
		if definition.AnchorAt == nil || definition.IntervalSeconds == nil {
			return nil, fmt.Errorf("interval schedule %q requires anchorAt and intervalSeconds", definition.ID)
		}
		next, err := nextIntervalOccurrence(*definition.AnchorAt, *definition.IntervalSeconds, after)
		if err != nil {
			return nil, err
		}
		return &next, nil
	case SourceTypeCron:
		next, err := nextCronOccurrenceAfter(definition.CronExpression, definition.Timezone, after)
		if err != nil {
			return nil, err
		}
		return &next, nil
	case SourceTypeOneShotAbsolute, SourceTypeOneShotRelative:
		return nil, nil
	default:
		return nil, fmt.Errorf("unsupported schedule source type %q", definition.SourceType)
	}
}

func nextIntervalOccurrence(anchor time.Time, intervalSeconds int64, after time.Time) (time.Time, error) {
	if intervalSeconds <= 0 {
		return time.Time{}, fmt.Errorf("intervalSeconds must be positive")
	}
	if intervalSeconds > int64((1<<63-1)/int64(time.Second)) {
		return time.Time{}, fmt.Errorf("intervalSeconds is too large")
	}

	anchorUTC := anchor.UTC()
	afterUTC := after.UTC()
	interval := time.Duration(intervalSeconds) * time.Second
	if afterUTC.Before(anchorUTC) {
		return anchorUTC, nil
	}

	elapsed := afterUTC.Sub(anchorUTC)
	steps := int64(elapsed / interval)
	next := anchorUTC.Add(time.Duration(steps+1) * interval)
	return next, nil
}

// NextIntervalOccurrence is the exported alias for nextIntervalOccurrence so
// other packages (workflow runtime dispatcher) can reuse the same arithmetic
// without duplicating it. The math is domain-agnostic; only the surrounding
// Definition/Trigger wrappers differ.
func NextIntervalOccurrence(anchor time.Time, intervalSeconds int64, after time.Time) (time.Time, error) {
	return nextIntervalOccurrence(anchor, intervalSeconds, after)
}

// NextCronOccurrenceAfter is the exported alias for nextCronOccurrenceAfter.
// Exposed for cross-package reuse (workflow runtime dispatcher).
func NextCronOccurrenceAfter(expression string, timezone string, after time.Time) (time.Time, error) {
	return nextCronOccurrenceAfter(expression, timezone, after)
}

type cronField struct {
	any     bool
	allowed map[int]struct{}
}

func (field cronField) matches(value int) bool {
	if field.any {
		return true
	}
	_, ok := field.allowed[value]
	return ok
}

type cronSchedule struct {
	minute cronField
	hour   cronField
	day    cronField
	month  cronField
	week   cronField
}

func (schedule cronSchedule) matches(candidate time.Time) bool {
	if !schedule.minute.matches(candidate.Minute()) {
		return false
	}
	if !schedule.hour.matches(candidate.Hour()) {
		return false
	}
	if !schedule.month.matches(int(candidate.Month())) {
		return false
	}

	dayMatch := schedule.day.matches(candidate.Day())
	weekMatch := schedule.week.matches(int(candidate.Weekday()))
	switch {
	case schedule.day.any && schedule.week.any:
		return true
	case schedule.day.any:
		return weekMatch
	case schedule.week.any:
		return dayMatch
	default:
		return dayMatch || weekMatch
	}
}

func parseCronExpression(expression string) (cronSchedule, error) {
	parts := strings.Fields(strings.TrimSpace(expression))
	if len(parts) != 5 {
		return cronSchedule{}, fmt.Errorf("cron expression must have 5 fields")
	}

	minute, err := parseCronField(parts[0], 0, 59, false)
	if err != nil {
		return cronSchedule{}, fmt.Errorf("parse cron minute field: %w", err)
	}
	hour, err := parseCronField(parts[1], 0, 23, false)
	if err != nil {
		return cronSchedule{}, fmt.Errorf("parse cron hour field: %w", err)
	}
	day, err := parseCronField(parts[2], 1, 31, false)
	if err != nil {
		return cronSchedule{}, fmt.Errorf("parse cron day-of-month field: %w", err)
	}
	month, err := parseCronField(parts[3], 1, 12, false)
	if err != nil {
		return cronSchedule{}, fmt.Errorf("parse cron month field: %w", err)
	}
	week, err := parseCronField(parts[4], 0, 6, true)
	if err != nil {
		return cronSchedule{}, fmt.Errorf("parse cron day-of-week field: %w", err)
	}

	return cronSchedule{
		minute: minute,
		hour:   hour,
		day:    day,
		month:  month,
		week:   week,
	}, nil
}

func parseCronField(expression string, min int, max int, allowSundaySeven bool) (cronField, error) {
	trimmed := strings.TrimSpace(expression)
	if trimmed == "" {
		return cronField{}, fmt.Errorf("field is required")
	}
	if trimmed == "*" {
		return cronField{any: true}, nil
	}

	field := cronField{allowed: make(map[int]struct{})}
	for _, part := range strings.Split(trimmed, ",") {
		if err := addCronPart(field.allowed, strings.TrimSpace(part), min, max, allowSundaySeven); err != nil {
			return cronField{}, err
		}
	}
	if len(field.allowed) == 0 {
		return cronField{}, fmt.Errorf("field selects no values")
	}
	return field, nil
}

func addCronPart(allowed map[int]struct{}, expression string, min int, max int, allowSundaySeven bool) error {
	if expression == "" {
		return fmt.Errorf("empty field segment")
	}

	base := expression
	step := 1
	if slash := strings.Index(base, "/"); slash >= 0 {
		parsedStep, err := strconv.Atoi(strings.TrimSpace(base[slash+1:]))
		if err != nil || parsedStep <= 0 {
			return fmt.Errorf("invalid step %q", expression)
		}
		step = parsedStep
		base = strings.TrimSpace(base[:slash])
	}

	rangeStart := min
	rangeEnd := max
	switch {
	case base == "*":
	case strings.Contains(base, "-"):
		parts := strings.SplitN(base, "-", 2)
		if len(parts) != 2 {
			return fmt.Errorf("invalid range %q", expression)
		}
		start, err := parseCronValue(parts[0], min, max, allowSundaySeven)
		if err != nil {
			return err
		}
		end, err := parseCronValue(parts[1], min, max, allowSundaySeven)
		if err != nil {
			return err
		}
		if allowSundaySeven && (strings.TrimSpace(parts[0]) == "7" || strings.TrimSpace(parts[1]) == "7") {
			return fmt.Errorf("day-of-week ranges cannot include 7")
		}
		if start > end {
			return fmt.Errorf("invalid descending range %q", expression)
		}
		rangeStart = start
		rangeEnd = end
	default:
		value, err := parseCronValue(base, min, max, allowSundaySeven)
		if err != nil {
			return err
		}
		rangeStart = value
		rangeEnd = value
	}

	for value := rangeStart; value <= rangeEnd; value += step {
		normalized := value
		if allowSundaySeven && normalized == 7 {
			normalized = 0
		}
		allowed[normalized] = struct{}{}
	}
	return nil
}

func parseCronValue(raw string, min int, max int, allowSundaySeven bool) (int, error) {
	value, err := strconv.Atoi(strings.TrimSpace(raw))
	if err != nil {
		return 0, fmt.Errorf("invalid value %q", raw)
	}
	if allowSundaySeven && value == 7 {
		return 7, nil
	}
	if value < min || value > max {
		return 0, fmt.Errorf("value %d is out of range", value)
	}
	return value, nil
}

func nextCronOccurrenceAfter(expression string, timezone string, after time.Time) (time.Time, error) {
	schedule, err := parseCronExpression(expression)
	if err != nil {
		return time.Time{}, err
	}

	location, err := time.LoadLocation(strings.TrimSpace(timezone))
	if err != nil {
		return time.Time{}, fmt.Errorf("load cron timezone: %w", err)
	}

	candidate := after.UTC().Truncate(time.Minute).Add(time.Minute)
	for minute := 0; minute < maxCronSearchMinutes; minute++ {
		if schedule.matches(candidate.In(location)) {
			return candidate.UTC(), nil
		}
		candidate = candidate.Add(time.Minute)
	}
	return time.Time{}, fmt.Errorf("could not find next cron occurrence within search window")
}
