"""Type-casting utilities for RouterOS API values.

RouterOS returns all values as strings.  This module provides helpers
to cast those strings into native Python types.

Supported target types
----------------------
* ``str``       – identity (no conversion)
* ``int``       – ``int(value)``
* ``float``     – ``float(value)``
* ``bool``      – ``"true"``/``"yes"`` → ``True``; ``"false"``/``"no"`` → ``False``
* ``datetime.timedelta`` – parse RouterOS duration strings like ``"1w2d3h4m5s"``
* ``list``      – split on ``,`` (returns ``list[str]``)
* ``bytes``     – hex string (e.g. MAC address without colons)

Usage example
-------------
::

    from routeros.cast import cast, typed_map
    from routeros.sentence import Sentence

    # Single value
    mtu = cast("1500", int)      # → 1500
    running = cast("true", bool) # → True

    # Whole sentence with a schema
    sen: Sentence = ...
    row = typed_map(sen, {"address": str, "mtu": int, "running": bool})
"""

from __future__ import annotations

import re
from datetime import timedelta
from typing import Any, Type, TypeVar

T = TypeVar("T")

# Map from RouterOS bool strings to Python bool values.
_TRUE_VALUES = {"true", "yes", "1"}
_FALSE_VALUES = {"false", "no", "0", ""}

# RouterOS duration pattern: optional weeks/days/hours/minutes/seconds.
_DURATION_RE = re.compile(
    r"(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$"
)


def cast(value: str, to: Type[T]) -> T:
    """Cast a RouterOS string *value* to the Python type *to*.

    Raises :class:`ValueError` for unrecognised target types or values
    that cannot be converted.
    """
    if to is str:
        return value  # type: ignore[return-value]

    if to is int:
        return int(value)  # type: ignore[return-value]

    if to is float:
        return float(value)  # type: ignore[return-value]

    if to is bool:
        lower = value.lower()
        if lower in _TRUE_VALUES:
            return True  # type: ignore[return-value]
        if lower in _FALSE_VALUES:
            return False  # type: ignore[return-value]
        raise ValueError(f"cannot cast {value!r} to bool")

    if to is timedelta:
        return _parse_duration(value)  # type: ignore[return-value]

    if to is list:
        if value == "":
            return []  # type: ignore[return-value]
        return [v.strip() for v in value.split(",")]  # type: ignore[return-value]

    if to is bytes:
        # Accept hex strings with optional separators (: or -)
        cleaned = value.replace(":", "").replace("-", "")
        return bytes.fromhex(cleaned)  # type: ignore[return-value]

    raise ValueError(f"unsupported cast target type: {to!r}")


def cast_optional(value: str, to: Type[T]) -> T | None:
    """Like :func:`cast` but returns ``None`` when *value* is ``""``.

    ::

        cast_optional("", int)    # → None
        cast_optional("42", int)  # → 42
    """
    if value == "":
        return None
    return cast(value, to)


def typed_map(sentence: Any, schema: dict[str, Type[Any]]) -> dict[str, Any]:
    """Return a new dict with sentence values cast according to *schema*.

    Keys absent from the sentence map are omitted from the result.
    Keys in the schema that map to ``type(None)`` are passed through
    :func:`cast_optional`.

    Example::

        schema = {"address": str, "mtu": int, "running": bool}
        row = typed_map(sentence, schema)
        # row == {"address": "192.168.1.1/24", "mtu": 1500, "running": True}
    """
    result: dict[str, Any] = {}
    for key, target_type in schema.items():
        if key not in sentence.map:
            continue
        raw = sentence.map[key]
        if target_type is type(None):
            result[key] = cast_optional(raw, str)
        else:
            result[key] = cast(raw, target_type)
    return result


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _parse_duration(value: str) -> timedelta:
    """Parse a RouterOS duration string into a :class:`~datetime.timedelta`.

    RouterOS uses the format ``[Xw][Xd][Xh][Xm][Xs]`` where each component
    is optional.  Examples: ``"1w"``, ``"2d3h"``, ``"5m30s"``, ``"45"``.

    A plain integer (no unit suffix) is treated as seconds.
    """
    if not value:
        return timedelta(0)

    # Plain integer – treat as seconds.
    if value.isdigit():
        return timedelta(seconds=int(value))

    m = _DURATION_RE.fullmatch(value)
    if not m or all(g is None for g in m.groups()):
        raise ValueError(f"cannot parse RouterOS duration: {value!r}")

    weeks, days, hours, minutes, seconds = (int(g) if g is not None else 0 for g in m.groups())
    return timedelta(
        weeks=weeks,
        days=days,
        hours=hours,
        minutes=minutes,
        seconds=seconds,
    )
