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

from remedapy.decorator import make_data_last

Key = TypeVar('Key', str, int)
T = TypeVar('T')

Predicate = Callable[[T, Key], bool] | Callable[[T], bool] | Callable[[], bool]


@overload
def omit_by(data: dict[Key, T], predicate: Predicate[T, Key], /) -> dict[Key, T]: ...


@overload
def omit_by(predicate: Predicate[T, Key], /) -> Callable[[dict[Key, T]], dict[Key, T]]: ...


@make_data_last
def omit_by(data: dict[Key, T], predicate: Predicate[T, Key], /) -> dict[Key, T]:
    """
    Creates a new dict by removinf key-value pairs satisfying the predicate.

    The predicate can have arity:
    - 0 - accept no arguments
    - 1 - accept value
    - 2 - accept value and key

    Parameters
    ----------
    data: dict[K, V]
        The dict to omit key-value pairs from.
    predicate: Callable[[T, Key], bool] | Callable[[T], bool] | Callable[[], bool]
        The predicate to apply to the key-value pairs.

    Returns
    -------
    dict[K, V]
        The new dict with the omitted keys.

    See Also
    --------
    omit
    pick_by

    Examples
    --------
    Data first:
    >>> R.omit_by({'a': 1, 'b': 2, 'A': 3, 'B': 4}, lambda v, k: k == k.upper())
    {'a': 1, 'b': 2}
    >>> R.omit_by({'a': 1, 'b': 2, 'A': 3, 'B': 4}, R.gt(2))
    {'a': 1, 'b': 2}
    >>> R.omit_by({'a': 1, 'b': 2, 'A': 3, 'B': 4}, R.constant(False))
    {'a': 1, 'b': 2, 'A': 3, 'B': 4}
    >>> R.omit_by({'a': 1, 'b': 2, 'A': 3, 'B': 4}, R.constant(True))
    {}

    Data last:
    >>> R.omit_by(lambda v, k: k == k.upper())({'a': 1, 'b': 2, 'A': 3, 'B': 4})
    {'a': 1, 'b': 2}
    >>> R.omit_by(R.gt(2))({'a': 1, 'b': 2, 'A': 3, 'B': 4})
    {'a': 1, 'b': 2}
    >>> R.omit_by(R.constant(False))({'a': 1, 'b': 2, 'A': 3, 'B': 4})
    {'a': 1, 'b': 2, 'A': 3, 'B': 4}
    >>> R.omit_by(R.constant(True))({'a': 1, 'b': 2, 'A': 3, 'B': 4})
    {}

    """
    sig = inspect.signature(predicate)
    param_count = len(sig.parameters.values())
    if param_count > 1:
        predicate = cast(Callable[[T, Key], bool], predicate)
        return {k: v for k, v in data.items() if not predicate(v, k)}
    elif param_count == 1:
        predicate = cast(Callable[[T], bool], predicate)
        return {k: v for k, v in data.items() if not predicate(v)}
    else:
        predicate = cast(Callable[[], bool], predicate)
        return {k: v for k, v in data.items() if not predicate()}
