"""Parser de expresiones cron estandar de 5 campos."""

from __future__ import annotations

_ALIASES: dict[str, str] = {
    "@yearly": "0 0 1 1 *",
    "@annually": "0 0 1 1 *",
    "@monthly": "0 0 1 * *",
    "@weekly": "0 0 * * 0",
    "@daily": "0 0 * * *",
    "@midnight": "0 0 * * *",
    "@hourly": "0 * * * *",
}

# (lo, hi) inclusive for each field: minute, hour, day_of_month, month, day_of_week
_FIELD_LIMITS: list[tuple[int, int]] = [
    (0, 59),
    (0, 23),
    (1, 31),
    (1, 12),
    (0, 6),
]
_FIELD_NAMES = ["minute", "hour", "day_of_month", "month", "day_of_week"]


def parse_cron_expr(expr: str) -> dict[str, list[int]]:
    """Parsea una expresion cron estandar de 5 campos.

    Soporta *, rangos (1-5), listas (1,3,5), pasos (*/15) y aliases
    (@hourly, @daily, @weekly, @monthly, @yearly).

    Args:
        expr: Expresion cron de 5 campos o alias.

    Returns:
        Dict con claves minute, hour, day_of_month, month, day_of_week,
        cada una con la lista de enteros validos para ese campo.

    Raises:
        ValueError: Si la expresion es invalida o un valor esta fuera de rango.
    """
    expr = expr.strip()
    expr = _ALIASES.get(expr, expr)
    fields = expr.split()
    if len(fields) != 5:
        raise ValueError(
            f"parse_cron_expr: expected 5 fields, got {len(fields)} in {expr!r}"
        )
    result: dict[str, list[int]] = {}
    for i, field in enumerate(fields):
        lo, hi = _FIELD_LIMITS[i]
        key = _FIELD_NAMES[i]
        result[key] = _parse_field(field, lo, hi, key)
    return result


def _parse_field(field: str, lo: int, hi: int, name: str) -> list[int]:
    if field == "*":
        return list(range(lo, hi + 1))
    seen: set[int] = set()
    values: list[int] = []
    for part in field.split(","):
        for v in _parse_part(part, lo, hi):
            if v not in seen:
                seen.add(v)
                values.append(v)
    values.sort()
    return values


def _parse_part(part: str, lo: int, hi: int) -> list[int]:
    if "/" in part:
        base, step_str = part.split("/", 1)
        try:
            step = int(step_str)
        except ValueError:
            raise ValueError(f"invalid step {step_str!r}")
        if step <= 0:
            raise ValueError(f"step must be positive, got {step}")
        if base == "*":
            start, end = lo, hi
        elif "-" in base:
            start, end = _parse_range(base, lo, hi)
        else:
            start = _parse_value(base, lo, hi)
            end = hi
        return list(range(start, end + 1, step))
    if "-" in part:
        start, end = _parse_range(part, lo, hi)
        return list(range(start, end + 1))
    return [_parse_value(part, lo, hi)]


def _parse_range(s: str, lo: int, hi: int) -> tuple[int, int]:
    parts = s.split("-", 1)
    if len(parts) != 2:
        raise ValueError(f"invalid range {s!r}")
    start = _parse_value(parts[0], lo, hi)
    end = _parse_value(parts[1], lo, hi)
    if start > end:
        raise ValueError(f"range start {start} > end {end}")
    return start, end


def _parse_value(s: str, lo: int, hi: int) -> int:
    try:
        v = int(s)
    except ValueError:
        raise ValueError(f"invalid value {s!r}: not an integer")
    if v < lo or v > hi:
        raise ValueError(f"value {v} out of range [{lo}, {hi}]")
    return v
