namespace ClawdDotNet.Core.Scheduling;

/// <summary>
/// Einfaches Cron-Parsing für 5-Felder-Ausdrücke: Minute Stunde Tag Monat Wochentag
/// Unterstützt: Zahlen, Wildcards (*), Bereiche (1-5), Listen (1,3,5), Schritte (*/5)
/// </summary>
public sealed class CronExpression
{
    private readonly HashSet<int> _minutes;
    private readonly HashSet<int> _hours;
    private readonly HashSet<int> _daysOfMonth;
    private readonly HashSet<int> _months;
    private readonly HashSet<int> _daysOfWeek;

    private CronExpression(
        HashSet<int> minutes, HashSet<int> hours,
        HashSet<int> daysOfMonth, HashSet<int> months,
        HashSet<int> daysOfWeek)
    {
        _minutes = minutes;
        _hours = hours;
        _daysOfMonth = daysOfMonth;
        _months = months;
        _daysOfWeek = daysOfWeek;
    }

    public static CronExpression Parse(string expression)
    {
        var parts = expression.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
        if (parts.Length != 5)
            throw new FormatException($"Cron expression must have 5 fields, got {parts.Length}: '{expression}'");

        return new CronExpression(
            ParseField(parts[0], 0, 59),
            ParseField(parts[1], 0, 23),
            ParseField(parts[2], 1, 31),
            ParseField(parts[3], 1, 12),
            ParseField(parts[4], 0, 6)
        );
    }

    public bool Matches(DateTime dt)
    {
        return _minutes.Contains(dt.Minute)
            && _hours.Contains(dt.Hour)
            && _daysOfMonth.Contains(dt.Day)
            && _months.Contains(dt.Month)
            && _daysOfWeek.Contains((int)dt.DayOfWeek);
    }

    public DateTime? GetNextOccurrence(DateTime after)
    {
        var candidate = new DateTime(after.Year, after.Month, after.Day, after.Hour, after.Minute, 0)
            .AddMinutes(1);

        // Suche maximal 2 Jahre in die Zukunft
        var limit = after.AddYears(2);

        while (candidate < limit)
        {
            if (Matches(candidate))
                return candidate;

            candidate = candidate.AddMinutes(1);

            // Optimierung: überspringe ungültige Stunden/Tage
            if (!_months.Contains(candidate.Month))
            {
                candidate = new DateTime(candidate.Year, candidate.Month, 1).AddMonths(1);
                continue;
            }
            if (!_daysOfMonth.Contains(candidate.Day) || !_daysOfWeek.Contains((int)candidate.DayOfWeek))
            {
                candidate = new DateTime(candidate.Year, candidate.Month, candidate.Day).AddDays(1);
                continue;
            }
            if (!_hours.Contains(candidate.Hour))
            {
                candidate = new DateTime(candidate.Year, candidate.Month, candidate.Day, candidate.Hour, 0, 0)
                    .AddHours(1);
            }
        }

        return null;
    }

    private static HashSet<int> ParseField(string field, int min, int max)
    {
        var result = new HashSet<int>();

        foreach (var part in field.Split(','))
        {
            if (part.Length == 0)
                throw Bad(field, "leeres Teilfeld");

            if (part == "*")
            {
                for (var i = min; i <= max; i++) result.Add(i);
            }
            else if (part.Contains('/'))
            {
                var split = part.Split('/');
                if (split.Length != 2)
                    throw Bad(field, "Schrittangabe erwartet die Form 'basis/schritt'");

                var start = split[0] == "*" ? min : ParseNumber(field, split[0], min, max);
                var step = ParseNumber(field, split[1], 1, max); // Schritt 0 wäre eine Endlosschleife
                for (var i = start; i <= max; i += step) result.Add(i);
            }
            else if (part.Contains('-'))
            {
                var split = part.Split('-');
                if (split.Length != 2)
                    throw Bad(field, "Bereich erwartet die Form 'von-bis'");

                var from = ParseNumber(field, split[0], min, max);
                var to = ParseNumber(field, split[1], min, max);
                if (from > to)
                    throw Bad(field, $"Bereich {from}-{to} ist rückwärts");

                for (var i = from; i <= to; i++) result.Add(i);
            }
            else
            {
                result.Add(ParseNumber(field, part, min, max));
            }
        }

        return result;
    }

    private static int ParseNumber(string field, string value, int min, int max)
    {
        if (!int.TryParse(value, out var n))
            throw Bad(field, $"'{value}' ist keine Zahl");
        if (n < min || n > max)
            throw Bad(field, $"{n} liegt außerhalb von {min}-{max}");
        return n;
    }

    private static FormatException Bad(string field, string reason)
        => new($"Ungültiges Cron-Feld '{field}': {reason}.");
}
