package scheduler

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

type Expression struct{ fields [5]field }
type field map[int]bool

var bounds = [5][2]int{{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 6}}

func Parse(spec string) (Expression, error) {
	parts := strings.Fields(spec)
	if len(parts) != 5 {
		return Expression{}, fmt.Errorf("cron requires five fields")
	}
	var expression Expression
	for i, part := range parts {
		parsed, err := parseField(part, bounds[i][0], bounds[i][1])
		if err != nil {
			return Expression{}, fmt.Errorf("cron field %d: %w", i+1, err)
		}
		expression.fields[i] = parsed
	}
	return expression, nil
}

func parseField(value string, min, max int) (field, error) {
	result := field{}
	for _, segment := range strings.Split(value, ",") {
		step := 1
		base := segment
		if strings.Contains(segment, "/") {
			parts := strings.Split(segment, "/")
			if len(parts) != 2 {
				return nil, fmt.Errorf("invalid step %q", segment)
			}
			base = parts[0]
			var err error
			step, err = strconv.Atoi(parts[1])
			if err != nil || step < 1 {
				return nil, fmt.Errorf("invalid step %q", parts[1])
			}
		}
		start, end := min, max
		if base != "*" {
			if strings.Contains(base, "-") {
				parts := strings.Split(base, "-")
				if len(parts) != 2 {
					return nil, fmt.Errorf("invalid range %q", base)
				}
				start, _ = strconv.Atoi(parts[0])
				end, _ = strconv.Atoi(parts[1])
			} else {
				var err error
				start, err = strconv.Atoi(base)
				end = start
				if err != nil {
					return nil, fmt.Errorf("invalid value %q", base)
				}
			}
		}
		if start < min || end > max || start > end {
			return nil, fmt.Errorf("value outside %d-%d", min, max)
		}
		for n := start; n <= end; n += step {
			result[n] = true
		}
	}
	return result, nil
}

func (e Expression) Matches(t time.Time) bool {
	values := [5]int{t.Minute(), t.Hour(), t.Day(), int(t.Month()), int(t.Weekday())}
	for i, value := range values {
		if !e.fields[i][value] {
			return false
		}
	}
	return true
}

func (e Expression) Next(after time.Time) time.Time {
	t := after.Truncate(time.Minute).Add(time.Minute)
	limit := t.AddDate(2, 0, 0)
	for t.Before(limit) {
		if e.Matches(t) {
			return t
		}
		t = t.Add(time.Minute)
	}
	return time.Time{}
}
