from collections.abc import Callable
from typing import ParamSpec, TypeVar, cast, overload

import remedapy as R
from remedapy.decorator import make_data_last

T = TypeVar('T')
ReturnType = TypeVar('ReturnType')
P = ParamSpec('P')
Z = TypeVar('Z')

Predicate = Callable[[T], bool] | Callable[[], bool]
OnTrue = Callable[[T], ReturnType] | Callable[[], ReturnType]
Conditions = list[tuple[Predicate[T], OnTrue[T, ReturnType]] | OnTrue[T, ReturnType]]


@overload
def conditional(
    data: T,
    conditions: Conditions[T, ReturnType],
    /,
) -> ReturnType: ...


@overload
def conditional(
    conditions: Conditions[T, ReturnType],
    /,
) -> Callable[[T], ReturnType]: ...


@make_data_last
def conditional(
    data: T,
    conditions: Conditions[T, ReturnType],
    /,
) -> ReturnType:
    """
    For data and a list of (predicate, transformer) executes the first transformer for which the predicate is True.

    Acts like a switch statement or if-elif-else construct.

    The condition can be only a transformers instead of a tuple in which case it is always used if reached.

    Parameters
    ----------
    data: T
        The value on which to test the predicates and execute the transformers.
    conditions: Conditions[T, ReturnType]
        List of (predicate, transformer) tuples.

    Returns
    -------
    ReturnType
        The result of the first transformer for which the predicate is True.

    Examples
    --------
    Data first:
    >>> R.conditional(
    ...     3,
    ...     [
    ...         (R.is_string, lambda name: f'Hello {name}'),
    ...         (R.is_number, lambda id: f'Hello ID: {id}'),
    ...         R.constant(None),
    ...     ],
    ... )
    'Hello ID: 3'

    Data last:
    >>> R.conditional(
    ...     [
    ...         (R.is_string, lambda name: f'Hello {name}'),
    ...         (R.is_number, lambda id: f'Hello ID: {id}'),
    ...     ],
    ... )('a')
    'Hello a'

    """
    apply_to_data = R.apply(data)
    for condition in conditions:
        if not isinstance(condition, tuple):
            return apply_to_data(condition)
        predicate = cast(Predicate[T], condition[0])
        onTrue = cast(OnTrue[T, ReturnType], condition[1])
        is_true = apply_to_data(predicate)
        if is_true:
            return apply_to_data(onTrue)
    # TODO: parametrise
    raise ValueError('No condition matched')  # pragma: no cover
