"""Transform registry and built-in transforms for the expression template pipeline DSL.

Built-in transforms cover date/time extraction, rounding, arithmetic, type casting,
string operations, and basic math. Custom transforms can be registered at runtime.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any


class TransformRegistry:
    """Registry of named transform functions for use in
    :class:`~therismos.expr.template.ParamRule` pipelines.

    :Example:
        >>> registry = TransformRegistry()
        >>> registry.register("double", lambda x: x * 2)
        >>> registry.apply("double", 5)
        10
    """

    def __init__(self) -> None:
        self._registry: dict[str, Callable[..., Any]] = {}

    def register(self, name: str, fn: Callable[..., Any]) -> None:
        """Register a named transform function.

        :param name: Transform name used in pipeline DSL strings.
        :type name: str
        :param fn: Callable ``fn(value, *args) -> Any``.
        :type fn: Callable[..., Any]
        """
        self._registry[name] = fn

    def register_decorator(self, name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
        """Decorator factory for registering a transform function.

        :param name: Transform name.
        :type name: str
        :return: Decorator that registers and returns the function.
        :rtype: Callable

        :Example:
            >>> @registry.register_decorator("triple")
            ... def triple(x):
            ...     return x * 3
        """

        def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
            self.register(name, fn)
            return fn

        return decorator

    def apply(self, name: str, value: Any, *args: Any) -> Any:
        """Apply a registered transform to a value.

        :param name: Transform name.
        :type name: str
        :param value: Input value.
        :param args: Additional arguments forwarded to the transform function.
        :return: Transformed value.
        :raises KeyError: If ``name`` is not registered.
        """
        if name not in self._registry:
            raise KeyError(f"Unknown transform: {name!r}. Available: {sorted(self._registry)}")
        return self._registry[name](value, *args)

    def names(self) -> set[str]:
        """Return the set of all registered transform names.

        :return: Set of transform name strings.
        :rtype: set[str]
        """
        return set(self._registry)


DEFAULT_TRANSFORM_REGISTRY = TransformRegistry()
"""The global default :class:`TransformRegistry` pre-populated with all built-in transforms."""

_r = DEFAULT_TRANSFORM_REGISTRY


def _extract_date(value: Any) -> Any:
    """Extract the date component from a datetime or return value if already a date."""
    import datetime

    if isinstance(value, datetime.datetime):
        return value.date()
    if isinstance(value, datetime.date):
        return value
    raise TypeError(f"extract_date: expected datetime or date, got {type(value).__name__}")


def _extract_time(value: Any) -> Any:
    """Extract the time component from a datetime."""
    import datetime

    if isinstance(value, datetime.datetime):
        return value.time()
    raise TypeError(f"extract_time: expected datetime, got {type(value).__name__}")


def _extract_year(value: Any) -> int:
    """Extract the year component from a date or datetime value."""
    import datetime

    if isinstance(value, (datetime.date, datetime.datetime)):
        return value.year
    raise TypeError(f"extract_year: expected date/datetime, got {type(value).__name__}")


def _extract_month(value: Any) -> int:
    """Extract the month component from a date or datetime value."""
    import datetime

    if isinstance(value, (datetime.date, datetime.datetime)):
        return value.month
    raise TypeError(f"extract_month: expected date/datetime, got {type(value).__name__}")


def _extract_day(value: Any) -> int:
    """Extract the day-of-month component from a date or datetime value."""
    import datetime

    if isinstance(value, (datetime.date, datetime.datetime)):
        return value.day
    raise TypeError(f"extract_day: expected date/datetime, got {type(value).__name__}")


_r.register("extract_date", _extract_date)
_r.register("extract_time", _extract_time)
_r.register("extract_year", _extract_year)
_r.register("extract_month", _extract_month)
_r.register("extract_day", _extract_day)


def _start_of_day(value: Any) -> Any:
    """Return midnight (00:00:00) of the given date or datetime."""
    import datetime

    if isinstance(value, datetime.datetime):
        return value.replace(hour=0, minute=0, second=0, microsecond=0)
    if isinstance(value, datetime.date):
        return value
    raise TypeError(f"start_of_day: expected date/datetime, got {type(value).__name__}")


def _end_of_day(value: Any) -> Any:
    """Return 23:59:59.999999 of the given date or datetime."""
    import datetime

    if isinstance(value, datetime.datetime):
        return value.replace(hour=23, minute=59, second=59, microsecond=999999)
    if isinstance(value, datetime.date):
        return value
    raise TypeError(f"end_of_day: expected date/datetime, got {type(value).__name__}")


def _start_of_week(value: Any) -> Any:
    """Return the Monday of the week containing *value*."""
    import datetime

    if isinstance(value, datetime.datetime):
        d = value.date()
        monday = d - datetime.timedelta(days=d.weekday())
        return datetime.datetime.combine(monday, datetime.time.min)
    if isinstance(value, datetime.date):
        return value - datetime.timedelta(days=value.weekday())
    raise TypeError(f"start_of_week: expected date/datetime, got {type(value).__name__}")


def _end_of_week(value: Any) -> Any:
    """Return the Sunday of the week containing *value*."""
    import datetime

    if isinstance(value, datetime.datetime):
        d = value.date()
        sunday = d + datetime.timedelta(days=6 - d.weekday())
        return datetime.datetime.combine(sunday, datetime.time(23, 59, 59, 999999))
    if isinstance(value, datetime.date):
        return value + datetime.timedelta(days=6 - value.weekday())
    raise TypeError(f"end_of_week: expected date/datetime, got {type(value).__name__}")


def _start_of_month(value: Any) -> Any:
    """Return the first day of the month for the given date or datetime."""
    import datetime

    if isinstance(value, datetime.datetime):
        return value.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
    if isinstance(value, datetime.date):
        return value.replace(day=1)
    raise TypeError(f"start_of_month: expected date/datetime, got {type(value).__name__}")


def _end_of_month(value: Any) -> Any:
    """Return the last moment of the month for the given date or datetime."""
    import calendar
    import datetime

    if isinstance(value, datetime.datetime):
        last_day = calendar.monthrange(value.year, value.month)[1]
        return value.replace(day=last_day, hour=23, minute=59, second=59, microsecond=999999)
    if isinstance(value, datetime.date):
        last_day = calendar.monthrange(value.year, value.month)[1]
        return value.replace(day=last_day)
    raise TypeError(f"end_of_month: expected date/datetime, got {type(value).__name__}")


_r.register("start_of_day", _start_of_day)
_r.register("end_of_day", _end_of_day)
_r.register("start_of_week", _start_of_week)
_r.register("end_of_week", _end_of_week)
_r.register("start_of_month", _start_of_month)
_r.register("end_of_month", _end_of_month)


def _add_time(value: Any, duration: Any) -> Any:
    """Add a :class:`datetime.timedelta` to a date or datetime value."""
    return value + duration


def _sub_time(value: Any, duration: Any) -> Any:
    """Subtract a :class:`datetime.timedelta` from a date or datetime value."""
    return value - duration


_r.register("add_time", _add_time)
_r.register("sub_time", _sub_time)


def _as_int(value: Any) -> int:
    """Cast value to int."""
    return int(value)


def _as_float(value: Any) -> float:
    """Cast value to float."""
    return float(value)


def _as_str(value: Any) -> str:
    """Cast value to str."""
    return str(value)


def _as_date(value: Any) -> Any:
    import datetime

    if isinstance(value, datetime.datetime):
        return value.date()
    if isinstance(value, datetime.date):
        return value
    if isinstance(value, str):
        return datetime.date.fromisoformat(value)
    raise TypeError(f"as_date: cannot convert {type(value).__name__} to date")


def _as_datetime(value: Any) -> Any:
    import datetime

    if isinstance(value, datetime.datetime):
        return value
    if isinstance(value, datetime.date):
        return datetime.datetime(value.year, value.month, value.day)
    if isinstance(value, str):
        return datetime.datetime.fromisoformat(value)
    raise TypeError(f"as_datetime: cannot convert {type(value).__name__} to datetime")


_r.register("as_int", _as_int)
_r.register("as_float", _as_float)
_r.register("as_str", _as_str)
_r.register("as_date", _as_date)
_r.register("as_datetime", _as_datetime)


def _lower(value: Any) -> str:
    """Convert value to lowercase string."""
    return str(value).lower()


def _upper(value: Any) -> str:
    """Convert value to uppercase string."""
    return str(value).upper()


def _strip(value: Any) -> str:
    """Strip leading and trailing whitespace from value."""
    return str(value).strip()


_r.register("lower", _lower)
_r.register("upper", _upper)
_r.register("strip", _strip)


def _add(value: Any, n: Any) -> Any:
    """Add n to value."""
    return value + n


def _sub(value: Any, n: Any) -> Any:
    """Subtract n from value."""
    return value - n


def _mul(value: Any, n: Any) -> Any:
    """Multiply value by n."""
    return value * n


def _div(value: Any, n: Any) -> Any:
    """Divide value by n."""
    return value / n


def _abs(value: Any) -> Any:
    """Return the absolute value."""
    return abs(value)


def _round(value: Any, n: int = 0) -> Any:
    """Round value to n decimal places."""
    return round(value, int(n))


_r.register("add", _add)
_r.register("sub", _sub)
_r.register("mul", _mul)
_r.register("div", _div)
_r.register("abs", _abs)
_r.register("round", _round)
