from __future__ import annotations

from collections.abc import Callable, Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from functools import wraps
from re import search
from typing import Any, TypeVar, cast

from typing_extensions import override

from utilities.text import EnsureStrError, ensure_str


@dataclass(frozen=True, kw_only=True)
class ImpossibleCaseError(Exception):
    case: list[str]

    @override
    def __str__(self) -> str:
        desc = ", ".join(self.case)
        return f"Case must be possible: {desc}."


@contextmanager
def redirect_error(
    old: type[Exception] | tuple[type[Exception], ...],
    new: Exception | type[Exception],
    /,
    *,
    match: str | None = None,
) -> Iterator[None]:
    """Context-manager for redirecting a specific type of error."""

    try:
        yield
    except Exception as error:
        if not isinstance(error, old):
            raise
        if match is None:
            raise new from error
        match error.args:  #  do not import from utilities.iterables
            case (arg,):
                pass
            case _:
                raise _RedirectErrorNonUniqueArgError(
                    old=old, new=new, match=match, args=error.args
                ) from None
        try:
            msg = ensure_str(arg)
        except EnsureStrError:
            raise _RedirectErrorArgNotStringError(
                old=old, new=new, match=match, arg=arg
            ) from None
        if search(match, msg):
            raise new from error
        raise


@dataclass(frozen=True, kw_only=True)
class RedirectErrorError(Exception):
    old: type[Exception] | tuple[type[Exception], ...]
    new: Exception | type[Exception]
    match: str | None = None


@dataclass(frozen=True, kw_only=True)
class _RedirectErrorNonUniqueArgError(RedirectErrorError):
    args: tuple[Any, ...]

    @override
    def __str__(self) -> str:
        return f"Error must contain a unique argument; got {self.args}."


@dataclass(frozen=True, kw_only=True)
class _RedirectErrorArgNotStringError(RedirectErrorError):
    arg: Any

    @override
    def __str__(self) -> str:
        return f"Error argument must be a string; got {self.arg}."


_T = TypeVar("_T")
_TExc = TypeVar("_TExc", bound=Exception)


def retry(
    func: Callable[[], _T],
    error: type[Exception] | tuple[type[Exception], ...],
    callback: Callable[[_TExc], None],
    /,
    *,
    predicate: Callable[[_TExc], bool] | None = None,
) -> Callable[[], _T]:
    """Retry a function if an error is caught after the callback."""

    @wraps(func)
    def inner() -> _T:
        try:
            return func()
        except error as caught:
            caught = cast(_TExc, caught)
            if (predicate is None) or predicate(caught):
                callback(caught)
                return func()
            raise

    return inner


__all__ = ["ImpossibleCaseError", "RedirectErrorError", "redirect_error", "retry"]
