// Package cron fournit un scheduler pour les tâches planifiées.
// Supporte le format cron standard (* * * * *) avec timezone.
package cron

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

// Schedule représente une expression cron parsée.
type Schedule struct {
	Minute     []int // 0-59
	Hour       []int // 0-23
	DayOfMonth []int // 1-31
	Month      []int // 1-12
	DayOfWeek  []int // 0-6 (0=dimanche)
	Location   *time.Location
}

// ParseSchedule parse une expression cron standard.
// Format: "minute hour day month weekday"
// Exemples:
//   - "0 8 * * 1-5" : 8h00 du lundi au vendredi
//   - "*/15 * * * *" : toutes les 15 minutes
//   - "0 9 1 * *" : 9h00 le premier de chaque mois
func ParseSchedule(expr string, location *time.Location) (*Schedule, error) {
	if location == nil {
		location = time.UTC
	}

	parts := strings.Fields(expr)
	if len(parts) != 5 {
		return nil, fmt.Errorf("invalid cron expression: expected 5 fields, got %d", len(parts))
	}

	s := &Schedule{Location: location}
	var err error

	// Minute (0-59)
	s.Minute, err = parseField(parts[0], 0, 59)
	if err != nil {
		return nil, fmt.Errorf("minute: %w", err)
	}

	// Hour (0-23)
	s.Hour, err = parseField(parts[1], 0, 23)
	if err != nil {
		return nil, fmt.Errorf("hour: %w", err)
	}

	// Day of month (1-31)
	s.DayOfMonth, err = parseField(parts[2], 1, 31)
	if err != nil {
		return nil, fmt.Errorf("day of month: %w", err)
	}

	// Month (1-12)
	s.Month, err = parseField(parts[3], 1, 12)
	if err != nil {
		return nil, fmt.Errorf("month: %w", err)
	}

	// Day of week (0-6, 0=Sunday)
	s.DayOfWeek, err = parseField(parts[4], 0, 6)
	if err != nil {
		return nil, fmt.Errorf("day of week: %w", err)
	}

	return s, nil
}

// parseField parse un champ cron individuel.
// Supporte: *, */n, n, n-m, n,m,o
func parseField(field string, min, max int) ([]int, error) {
	var values []int

	// Gérer les listes (ex: "1,3,5")
	for _, part := range strings.Split(field, ",") {
		part = strings.TrimSpace(part)

		// Step (*/n ou n-m/s)
		step := 1
		if idx := strings.Index(part, "/"); idx != -1 {
			stepStr := part[idx+1:]
			var err error
			step, err = strconv.Atoi(stepStr)
			if err != nil || step < 1 {
				return nil, fmt.Errorf("invalid step: %s", stepStr)
			}
			part = part[:idx]
		}

		// Range ou valeur
		var rangeMin, rangeMax int
		if part == "*" {
			rangeMin, rangeMax = min, max
		} else if idx := strings.Index(part, "-"); idx != -1 {
			var err error
			rangeMin, err = strconv.Atoi(part[:idx])
			if err != nil {
				return nil, fmt.Errorf("invalid range start: %s", part[:idx])
			}
			rangeMax, err = strconv.Atoi(part[idx+1:])
			if err != nil {
				return nil, fmt.Errorf("invalid range end: %s", part[idx+1:])
			}
		} else {
			val, err := strconv.Atoi(part)
			if err != nil {
				return nil, fmt.Errorf("invalid value: %s", part)
			}
			rangeMin, rangeMax = val, val
		}

		// Valider les bornes
		if rangeMin < min || rangeMax > max || rangeMin > rangeMax {
			return nil, fmt.Errorf("value out of range [%d-%d]: %d-%d", min, max, rangeMin, rangeMax)
		}

		// Générer les valeurs
		for v := rangeMin; v <= rangeMax; v += step {
			values = append(values, v)
		}
	}

	if len(values) == 0 {
		return nil, fmt.Errorf("no values")
	}

	return values, nil
}

// Next calcule la prochaine exécution après 'from'.
func (s *Schedule) Next(from time.Time) time.Time {
	// Convertir dans la timezone du schedule
	t := from.In(s.Location)

	// Commencer à la minute suivante
	t = t.Truncate(time.Minute).Add(time.Minute)

	// Limite de recherche (1 an)
	limit := t.Add(366 * 24 * time.Hour)

	for t.Before(limit) {
		// Vérifier le mois
		if !contains(s.Month, int(t.Month())) {
			// Passer au premier jour du mois suivant
			t = time.Date(t.Year(), t.Month()+1, 1, 0, 0, 0, 0, s.Location)
			continue
		}

		// Vérifier le jour du mois ET le jour de la semaine
		// En cron standard, si les deux sont spécifiés (non-*), c'est un OR
		dayMatch := contains(s.DayOfMonth, t.Day())
		weekdayMatch := contains(s.DayOfWeek, int(t.Weekday()))

		// Si les deux champs sont "*", les deux sont vrais
		// Si l'un est spécifié et pas l'autre, seul celui spécifié compte
		// Si les deux sont spécifiés, c'est OR (comportement cron standard)
		bothWildcard := len(s.DayOfMonth) == 31 && len(s.DayOfWeek) == 7
		if bothWildcard {
			// Les deux sont *, donc on accepte tous les jours
		} else if len(s.DayOfMonth) == 31 {
			// Jour du mois est *, seul le jour de semaine compte
			if !weekdayMatch {
				t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, s.Location)
				continue
			}
		} else if len(s.DayOfWeek) == 7 {
			// Jour de semaine est *, seul le jour du mois compte
			if !dayMatch {
				t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, s.Location)
				continue
			}
		} else {
			// Les deux sont spécifiés : OR
			if !dayMatch && !weekdayMatch {
				t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, s.Location)
				continue
			}
		}

		// Vérifier l'heure
		if !contains(s.Hour, t.Hour()) {
			// Passer à l'heure suivante
			t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour()+1, 0, 0, 0, s.Location)
			continue
		}

		// Vérifier la minute
		if !contains(s.Minute, t.Minute()) {
			// Passer à la minute suivante
			t = t.Add(time.Minute)
			continue
		}

		// Trouvé !
		return t
	}

	// Pas trouvé dans l'année, retourner zero
	return time.Time{}
}

// NextN retourne les N prochaines exécutions.
func (s *Schedule) NextN(from time.Time, n int) []time.Time {
	var times []time.Time
	t := from
	for i := 0; i < n; i++ {
		next := s.Next(t)
		if next.IsZero() {
			break
		}
		times = append(times, next)
		t = next
	}
	return times
}

// contains vérifie si une valeur est dans une liste.
func contains(list []int, val int) bool {
	for _, v := range list {
		if v == val {
			return true
		}
	}
	return false
}

// LoadLocation charge une timezone par nom (ex: "Europe/Paris").
func LoadLocation(name string) (*time.Location, error) {
	if name == "" || name == "UTC" {
		return time.UTC, nil
	}
	return time.LoadLocation(name)
}
