"""Calcula la proxima ejecucion de un cron schedule."""

from __future__ import annotations

from datetime import datetime, timedelta


def next_cron_time(schedule: dict, after: datetime) -> datetime | None:
    """Calcula la proxima ejecucion de un cron schedule despues de `after`.

    Avanza minuto a minuto saltando campos no coincidentes. Retorna None
    si no hay match en 366 dias (schedule imposible).

    Args:
        schedule: Dict con claves minute, hour, day_of_month, month, day_of_week,
                  cada una con lista de enteros validos. Producido por parse_cron_expr.
        after: Tiempo de referencia. El proximo tick sera estrictamente posterior.

    Returns:
        Proximo datetime que satisface el schedule, o None si no existe en 366 dias.
    """
    minutes: list[int] = schedule["minute"]
    hours: list[int] = schedule["hour"]
    doms: list[int] = schedule["day_of_month"]
    months: list[int] = schedule["month"]
    dows: list[int] = schedule["day_of_week"]

    # Truncate to minute and advance by 1.
    t = after.replace(second=0, microsecond=0) + timedelta(minutes=1)
    limit = after + timedelta(days=366)

    while t < limit:
        # Check month.
        if t.month not in months:
            t = _next_valid_month(t, months)
            if t is None:
                return None
            continue

        # Check day_of_month AND day_of_week (AND semantics).
        if t.day not in doms or t.weekday() not in _python_dows(dows):
            t = t.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
            continue

        # Check hour.
        if t.hour not in hours:
            nxt = _next_valid_hour(t, hours)
            if nxt is None:
                t = t.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
            else:
                t = nxt
            continue

        # Check minute.
        if t.minute not in minutes:
            nxt = _next_valid_minute(t, minutes)
            if nxt is None:
                t = t.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
            else:
                t = nxt
            continue

        return t

    return None


def _python_dows(dows: list[int]) -> set[int]:
    """Convert cron day-of-week (0=Sun) to Python weekday() (0=Mon, 6=Sun)."""
    result = set()
    for d in dows:
        # cron: 0=Sun,1=Mon,...,6=Sat
        # python weekday: 0=Mon,...,6=Sun
        result.add((d - 1) % 7)
    return result


def _next_valid_month(t: datetime, months: list[int]) -> datetime | None:
    month = t.month
    for m in months:
        if m > month:
            return t.replace(month=m, day=1, hour=0, minute=0, second=0, microsecond=0)
    # Wrap to next year.
    if months:
        try:
            return t.replace(year=t.year + 1, month=months[0], day=1, hour=0, minute=0, second=0, microsecond=0)
        except ValueError:
            return None
    return None


def _next_valid_hour(t: datetime, hours: list[int]) -> datetime | None:
    h = t.hour
    for hh in hours:
        if hh > h:
            return t.replace(hour=hh, minute=0, second=0, microsecond=0)
    return None


def _next_valid_minute(t: datetime, minutes: list[int]) -> datetime | None:
    m = t.minute
    for mm in minutes:
        if mm > m:
            return t.replace(minute=mm, second=0, microsecond=0)
    return None
