"""Factory for creating validated wrapper types around base types."""

from __future__ import annotations

import inspect
from collections.abc import Sequence
from functools import wraps
from typing import TYPE_CHECKING, Any

import attrs

if TYPE_CHECKING:
    from collections.abc import Callable


def new_type(
    name: str,
    base_type: type,
    validators: (
        Callable[..., Any] | Sequence[Callable[..., Any]] | None
    ) = None,
    converters: (
        Callable[..., Any] | Sequence[Callable[..., Any]] | None
    ) = None,
    *,
    frozen: bool = True,
) -> type:
    """
    Create a validated wrapper type around
    *base_type*.
    """
    kwargs = _callables_to_kwargs(base_type, validators, converters)

    @attrs.define(frozen=frozen, eq=True, hash=frozen)  # type: ignore[literal-required]
    class _Wrapper:
        """Validated wrapper around a base type instance."""

        inner: Any = attrs.field(**kwargs)

        def map(self, func: Callable[[Any], Any]) -> Any:
            """
            Apply *func* and return a new wrapper.
            """
            return self.__class__(func(self.inner))

        # Forward public methods from base_type
        # via locals() injection. This is
        # intentional dynamic dispatch.
        locals().update(_create_forward_methods(base_type))

    _Wrapper.__name__ = name
    _Wrapper.__qualname__ = name
    return _Wrapper


def _create_forward_methods(
    base_type: type,
) -> dict[str, Callable[..., Any]]:
    """
    Build forwarder methods for public methods
    of *base_type*.
    """
    methods: dict[str, Callable[..., Any]] = {}
    for attr_name, _ in inspect.getmembers(base_type, inspect.isroutine):
        if attr_name.startswith("_"):
            continue

        def make_forwarder(
            method_name: str,
        ) -> Callable[..., Any]:
            """Create a forwarder for a single method."""

            def method(
                self: Any,
                *args: Any,
                **kwargs: Any,
            ) -> Any:
                """Delegate to the wrapped object's method."""
                return getattr(self.inner, method_name)(*args, **kwargs)

            method.__name__ = method_name
            method.__doc__ = getattr(base_type, method_name).__doc__
            return method

        methods[attr_name] = make_forwarder(attr_name)
    return methods


def _callables_to_kwargs(
    base_type: type,
    validators: (Callable[..., Any] | Sequence[Callable[..., Any]] | None),
    converters: (Callable[..., Any] | Sequence[Callable[..., Any]] | None),
) -> dict[str, Any]:
    """
    Build attrs field kwargs from *validators*
    and *converters*.
    """
    kwargs: dict[str, list[Callable[..., Any]]] = {
        "validator": [attrs.validators.instance_of(base_type)],
        "converter": [],
    }
    kwargs["validator"] += [
        _validate_bool_func(fn) for fn in _to_list(validators)
    ]
    kwargs["converter"] += _to_list(converters)

    return {k: v for k, v in kwargs.items() if v}


def _validate_bool_func(
    bool_fn: Callable[..., bool],
) -> Callable[
    [Any, attrs.Attribute[Any], Any],
    None,
]:
    """
    Wrap *bool_fn* as an attrs validator.
    """
    if not callable(bool_fn):
        msg = "provided boolean function must be callable"
        raise TypeError(msg)

    @wraps(bool_fn)
    def wrapper(
        _instance: attrs.AttrsInstance,
        attribute: attrs.Attribute[Any],
        value: Any,
    ) -> None:
        """Validate that *bool_fn* returns True for *value*."""
        if not bool_fn(value):
            msg = (
                f"{attribute.name} does not"
                " return True for the given"
                " boolean function, received"
                f" `{value}`."
            )
            raise ValueError(msg)

    return wrapper


def _to_list(
    value: (Callable[..., Any] | Sequence[Callable[..., Any]] | None),
) -> list[Callable[..., Any]]:
    """
    Normalize *value* to a list of callables.
    """
    if value is None:
        return []

    if callable(value):
        return [value]

    if isinstance(value, Sequence) and not all(callable(fn) for fn in value):
        msg = f"Given items are not all callable: {value = }"
        raise TypeError(msg)

    return list(value)
