package gronx

import (
	"errors"
	"regexp"
	"strings"
	"time"
)

var literals = strings.NewReplacer(
	"SUN", "0", "MON", "1", "TUE", "2", "WED", "3", "THU", "4", "FRI", "5", "SAT", "6",
	"JAN", "1", "FEB", "2", "MAR", "3", "APR", "4", "MAY", "5", "JUN", "6", "JUL", "7",
	"AUG", "8", "SEP", "9", "OCT", "10", "NOV", "11", "DEC", "12",
)

var expressions = map[string]string{
	"@yearly":    "0 0 1 1 *",
	"@annually":  "0 0 1 1 *",
	"@monthly":   "0 0 1 * *",
	"@weekly":    "0 0 * * 0",
	"@daily":     "0 0 * * *",
	"@hourly":    "0 * * * *",
	"@always":    "* * * * *",
	"@5minutes":  "*/5 * * * *",
	"@10minutes": "*/10 * * * *",
	"@15minutes": "*/15 * * * *",
	"@30minutes": "0,30 * * * *",

	"@everysecond": "* * * * * *",
}

// AddTag adds a new custom tag representing given expr
func AddTag(tag, expr string) error {
	_, ok := expressions[tag]
	if ok {
		return errors.New("conflict tag")
	}

	segs, err := Segments(expr)
	if err != nil {
		return err
	}
	expr = strings.Join(segs, " ")

	expressions[tag] = expr
	return nil
}

// SpaceRe is regex for whitespace.
var SpaceRe = regexp.MustCompile(`\s+`)
var yearRe = regexp.MustCompile(`\d{4}`)

func normalize(expr string) []string {
	expr = strings.Trim(expr, " \t")
	if e, ok := expressions[strings.ToLower(expr)]; ok {
		expr = e
	}

	expr = SpaceRe.ReplaceAllString(expr, " ")
	expr = literals.Replace(strings.ToUpper(expr))

	return strings.Split(strings.ReplaceAll(expr, "  ", " "), " ")
}

// Gronx is the main program.
type Gronx struct {
	C Checker
}

// New initializes Gronx with factory defaults.
func New() *Gronx {
	return &Gronx{&SegmentChecker{}}
}

// IsDue checks if cron expression is due for given reference time (or now).
// It returns bool or error if any.
func (g *Gronx) IsDue(expr string, ref ...time.Time) (bool, error) {
	if len(ref) == 0 {
		ref = append(ref, time.Now())
	}
	g.C.SetRef(ref[0])

	segs, err := Segments(expr)
	if err != nil {
		return false, err
	}

	return g.SegmentsDue(segs)
}

func (g *Gronx) isDue(expr string, ref time.Time) bool {
	due, err := g.IsDue(expr, ref)
	return err == nil && due
}

// Segments splits expr into array array of cron parts.
// If expression contains 5 parts or 6th part is year like, it prepends a second.
// It returns array or error.
func Segments(expr string) ([]string, error) {
	segs := normalize(expr)
	slen := len(segs)
	if slen < 5 || slen > 7 {
		return []string{}, errors.New("expr should contain 5-7 segments separated by space")
	}

	// Prepend second if required
	prepend := slen == 5 || (slen == 6 && yearRe.MatchString(segs[5]))
	if prepend {
		segs = append([]string{"0"}, segs...)
	}

	return segs, nil
}

// SegmentsDue checks if all cron parts are due.
// It returns bool. You should use IsDue(expr) instead.
func (g *Gronx) SegmentsDue(segs []string) (bool, error) {
	skipMonthDayCheck := false
	for i := 0; i < len(segs); i++ {
		pos := len(segs) - 1 - i
		seg := segs[pos]
		isMonthDay, isWeekday := pos == 3, pos == 5

		if seg == "*" || seg == "?" {
			continue
		}

		if isMonthDay && skipMonthDayCheck {
			continue
		}

		if isWeekday {
			monthDaySeg := segs[3]
			intersect := strings.Index(seg, "*/") == 0 || strings.Index(monthDaySeg, "*") == 0 || monthDaySeg == "?"

			if !intersect {
				due, err := g.C.CheckDue(seg, pos)
				if err != nil {
					return false, err
				}

				monthDayDue, err := g.C.CheckDue(monthDaySeg, 3)
				if due || monthDayDue {
					skipMonthDayCheck = true
					continue
				}

				if err != nil {
					return false, err
				}
			}
		}

		if due, err := g.C.CheckDue(seg, pos); !due {
			return due, err
		}
	}

	return true, nil
}

// IsValid checks if cron expression is valid.
// It returns bool.
func (g *Gronx) IsValid(expr string) bool { return IsValid(expr) }

// checker for validity
var checker = &SegmentChecker{ref: time.Now()}

// IsValid checks if cron expression is valid.
// It returns bool.
func IsValid(expr string) bool {
	segs, err := Segments(expr)
	if err != nil {
		return false
	}

	// First check syntax without time dependency
	if !isSyntaxValid(segs) {
		return false
	}

	// Then check with time dependency
	for pos, seg := range segs {
		if _, err := checker.CheckDue(seg, pos); err != nil {
			return false
		}
	}

	return true
}

// isSyntaxValid checks if the cron segments are syntactically valid without time dependency.
// It returns bool.
func isSyntaxValid(segs []string) bool {
	for _, seg := range segs {
		// Check for empty segments
		if seg == "" {
			return false
		}

		// Split by comma to check each part
		parts := strings.Split(seg, ",")
		for _, part := range parts {
			// Check for empty parts
			if part == "" {
				return false
			}

			// Check for invalid characters
			if strings.ContainsAny(part, "*/") {
				// If contains /, must have a number after it
				if strings.Contains(part, "/") {
					parts := strings.Split(part, "/")
					if len(parts) != 2 || parts[1] == "" {
						return false
					}
				}
			}
		}
	}
	return true
}
