package cron

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

var (
	ErrAnyAlreadyGiven   = errors.New("* already given")
	ErrInvalidExpression = errors.New("invalid expression")
	ErrExceedsMinimum    = errors.New("exceeds minimum")
	ErrExceedsMaximum    = errors.New("exceeds maximum")
	ErrInvalidStepZero   = errors.New("step cannot be zero")
)

func Parse(input string, tz string) (Schedule, error) {
	loc := time.Local
	if tz != "" {
		var err error
		loc, err = time.LoadLocation(tz)
		if err != nil {
			return nil, fmt.Errorf("loading timezone: %w", err)
		}
	}

	var parser = [5]field{
		minuteExpr,
		hourExpr,
		domExpr,
		monthExpr,
		dowExpr,
	}

	f := strings.Fields(input)
	if len(f) != len(parser) {
		return nil, fmt.Errorf("invalid cron format")
	}

	var exprs [5]Expr
	for idx, input := range f {
		var err error
		exprs[idx], err = expression(parser[idx], input)
		if err != nil {
			return nil, err
		}
	}

	return schedule{
		loc:    loc,
		minute: exprs[0],
		hour:   exprs[1],
		dom:    exprs[2],
		month:  exprs[3],
		dow:    exprs[4],
	}, nil
}

type field struct {
	min   Value
	max   Value
	names map[string]Value
}

func (f field) Min() Value {
	return f.min
}

func (f field) Max() Value {
	return f.max
}

func (f field) Digit(input string) (Value, error) {
	var v Value

	input = strings.ToLower(input)

	v, ok := f.names[input]
	if !ok {
		i, err := strconv.ParseUint(input, 10, 8)
		if err != nil {
			return 0, fmt.Errorf("%w: %v", ErrInvalidExpression, input)
		} else {
			v = Value(i)
		}
	}

	if v < f.Min() {
		return v, fmt.Errorf("%w: %d", ErrExceedsMinimum, f.Min())
	}

	if v > f.Max() {
		return v, fmt.Errorf("%w: %d", ErrExceedsMaximum, f.Max())
	}

	return v, nil
}

func expression(f field, input string) (Expr, error) {
	var exprs Many
	var hasAny bool
	for _, co := range strings.Split(input, ",") {
		sl := strings.SplitN(co, "/", 2)
		if len(sl) == 2 {
			step, err := f.Digit(sl[1])
			if step == Value(0) {
				return exprs, ErrInvalidStepZero
			}
			if err != nil {
				return exprs, err
			}

			expr, err := unaryOrRangeExpression(f, sl[0])
			if err != nil {
				return exprs, err
			}

			exprs = append(exprs, Step{
				Range: expr,
				Step:  step,
			})
		} else {
			expr, err := unaryOrRangeExpression(f, co)
			if err != nil {
				return exprs, err
			}

			if _, ok := expr.(All); ok {
				if hasAny {
					return exprs, ErrAnyAlreadyGiven
				}
				hasAny = true
			}

			exprs = append(exprs, expr)
		}
	}

	return exprs, nil
}

func unaryOrRangeExpression(f field, input string) (Expr, error) {
	if input == "*" {
		return All{}, nil
	}

	da := strings.SplitN(input, "-", 2)
	if len(da) == 2 {
		from, err := f.Digit(da[0])
		if err != nil {
			return nil, err
		}

		to, err := f.Digit(da[1])
		if err != nil {
			return nil, err
		}

		return Range{
			From: from,
			To:   to,
		}, nil
	}

	return f.Digit(input)
}
