package controller

import (
	"fmt"
	"strings"
)

// cnpgShortcuts expands cron nickname shortcuts to the 6-field (seconds-first)
// cron expressions CloudNativePG's robfig/cron parser expects.
var cnpgShortcuts = map[string]string{
	"@hourly":   "0 0 * * * *",
	"@daily":    "0 0 0 * * *",
	"@midnight": "0 0 0 * * *",
	"@weekly":   "0 0 0 * * 0",
	"@monthly":  "0 0 0 1 * *",
	"@yearly":   "0 0 0 1 1 *",
	"@annually": "0 0 0 1 1 *",
}

// k8upShortcuts is the set of cron nicknames k8up accepts natively (passed
// through unchanged).
var k8upShortcuts = map[string]bool{
	"@hourly": true, "@daily": true, "@midnight": true, "@weekly": true,
	"@monthly": true, "@yearly": true, "@annually": true,
}

// ScheduleForK8up validates a schedule annotation for k8up, which accepts cron
// nickname shortcuts natively and otherwise expects a 5-field cron expression.
// A valid value is returned unchanged.
func ScheduleForK8up(in string) (string, error) {
	s := strings.TrimSpace(in)
	if s == "" {
		return "", fmt.Errorf("empty schedule")
	}
	if strings.HasPrefix(s, "@") {
		if k8upShortcuts[s] {
			return s, nil
		}
		return "", fmt.Errorf("unsupported schedule shortcut %q", s)
	}
	if err := validateCronFields(s, 5); err != nil {
		return "", err
	}
	return s, nil
}

// ScheduleForCNPG normalises a schedule annotation into the 6-field
// (seconds-first) cron format a CloudNativePG ScheduledBackup requires:
// shortcuts expand to explicit expressions, a 5-field cron gains a leading "0"
// seconds field, and a 6-field cron is accepted as-is.
func ScheduleForCNPG(in string) (string, error) {
	s := strings.TrimSpace(in)
	if s == "" {
		return "", fmt.Errorf("empty schedule")
	}
	if strings.HasPrefix(s, "@") {
		if v, ok := cnpgShortcuts[s]; ok {
			return v, nil
		}
		return "", fmt.Errorf("unsupported schedule shortcut %q", s)
	}
	fields := strings.Fields(s)
	switch len(fields) {
	case 5:
		if err := validateCronFields(s, 5); err != nil {
			return "", err
		}
		return "0 " + strings.Join(fields, " "), nil
	case 6:
		if err := validateCronFields(s, 6); err != nil {
			return "", err
		}
		return strings.Join(fields, " "), nil
	default:
		return "", fmt.Errorf("cron %q must have 5 or 6 fields, got %d", s, len(fields))
	}
}

// cronNames are the alphabetic month and day-of-week names robfig/cron (used by
// k8up and CloudNativePG) accepts, case-insensitively, e.g. "MON" or "JAN".
var cronNames = map[string]bool{
	"JAN": true, "FEB": true, "MAR": true, "APR": true, "MAY": true, "JUN": true,
	"JUL": true, "AUG": true, "SEP": true, "OCT": true, "NOV": true, "DEC": true,
	"SUN": true, "MON": true, "TUE": true, "WED": true, "THU": true, "FRI": true, "SAT": true,
}

func validateCronFields(s string, want int) error {
	fields := strings.Fields(s)
	if len(fields) != want {
		return fmt.Errorf("cron %q must have %d fields, got %d", s, want, len(fields))
	}
	for _, f := range fields {
		if f == "" {
			return fmt.Errorf("cron %q has an empty field", s)
		}
		if err := validateCronField(f); err != nil {
			return err
		}
	}
	return nil
}

// validateCronField accepts cron symbols plus known alphabetic month/day names
// (e.g. MON-FRI, JAN); an unknown alphabetic run like "foo" is rejected.
func validateCronField(f string) error {
	runes := []rune(f)
	for i := 0; i < len(runes); {
		if isLetter(runes[i]) {
			j := i
			for j < len(runes) && isLetter(runes[j]) {
				j++
			}
			name := strings.ToUpper(string(runes[i:j]))
			if !cronNames[name] {
				return fmt.Errorf("cron field %q contains unknown name %q", f, string(runes[i:j]))
			}
			i = j
			continue
		}
		if !isCronRune(runes[i]) {
			return fmt.Errorf("cron field %q contains invalid character %q", f, string(runes[i]))
		}
		i++
	}
	return nil
}

func isCronRune(r rune) bool {
	return (r >= '0' && r <= '9') || r == '*' || r == '/' || r == ',' || r == '-' || r == '?'
}

func isLetter(r rune) bool {
	return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}
