""":class:`ProviderABC` + :func:`translate_at_seam` — the provider boundary.

Implementation lives here; the package ``__init__`` re-exports.
"""

from __future__ import annotations

import functools
import inspect
from abc import ABC, abstractmethod
from typing import (
    TYPE_CHECKING,
    Any,
    Concatenate,
    ParamSpec,
    TypeVar,
    overload,
)

from mgf.common.exceptions import AppError

if TYPE_CHECKING:
    from collections.abc import Awaitable, Callable

P = ParamSpec("P")
R = TypeVar("R")
T_self = TypeVar("T_self")


class ProviderABC(ABC):
    """Abstract base for external-service provider adapters.

    Subclasses:

      - MUST implement :meth:`translate_exception`.
      - SHOULD wrap every external-call method with
        :func:`translate_at_seam` so non-:class:`AppError` exceptions
        from the underlying SDK become typed
        :class:`mgf.common.exceptions.AppError` subclasses at the
        seam.

    The class is deliberately minimal — no required state, no
    lifecycle methods. The contract is one method.
    """

    @abstractmethod
    def translate_exception(self, exc: Exception) -> AppError:
        """Translate a non-:class:`AppError` exception to a typed one.

        Called by :func:`translate_at_seam` when a wrapped method
        raises an exception that is NOT an :class:`AppError` subclass.
        Implementations should ``isinstance``-check against the
        underlying SDK's exception hierarchy and return an
        appropriate :class:`AppError` subclass:

        .. code-block:: python

            def translate_exception(self, exc: Exception) -> AppError:
                if isinstance(exc, sdk.NotFound):
                    return ResourceNotFoundError(name=str(exc))
                if isinstance(exc, sdk.RateLimited):
                    return ProviderError(
                        f"rate limited: {exc}",
                    )
                return ProviderError(
                    f"upstream call failed: {exc}",
                )

        Important:

          - DO NOT raise from this method. Return the
            :class:`AppError`; the decorator handles the raise +
            cause-chain.
          - DO NOT catch :class:`AppError` itself in the underlying
            method — let it pass through. The decorator only
            translates non-:class:`AppError` exceptions.

        The original exception is preserved as ``__cause__`` on the
        raised :class:`AppError` (set automatically by the decorator
        via ``raise translated from exc``), so debugging chains stay
        intact.
        """


# ---------------------------------------------------------------------------
# ``translate_at_seam`` decorator. Two overloads — async + sync — so type
# checkers see the right return shape per call site.
# ---------------------------------------------------------------------------


@overload
def translate_at_seam(
    method: Callable[Concatenate[T_self, P], Awaitable[R]],
) -> Callable[Concatenate[T_self, P], Awaitable[R]]: ...


@overload
def translate_at_seam(
    method: Callable[Concatenate[T_self, P], R],
) -> Callable[Concatenate[T_self, P], R]: ...


def translate_at_seam(method: Callable[..., Any]) -> Callable[..., Any]:
    """Decorator: translate non-:class:`AppError` exceptions at the seam.

    Apply to methods of :class:`ProviderABC` subclasses (or any class
    with a compatible :meth:`translate_exception` method). The
    wrapped method:

      - Lets :class:`AppError` subclasses pass through unchanged
        (they are already typed; nothing to translate).
      - Catches every other :class:`Exception`; calls
        ``self.translate_exception(exc)``; re-raises the typed
        :class:`AppError` with the original as ``__cause__`` (full
        chain preserved for debugging via ``e.__cause__``).
      - Lets :class:`BaseException` subclasses (:exc:`KeyboardInterrupt`,
        :exc:`SystemExit`, :exc:`GeneratorExit`, :exc:`MemoryError`,
        :exc:`asyncio.CancelledError`) pass through unchanged. We
        never catch those — that breaks Python's signal model.

    Works on both synchronous methods and ``async def`` methods —
    detected at decoration time via :func:`inspect.iscoroutinefunction`.
    """
    if inspect.iscoroutinefunction(method):

        @functools.wraps(method)
        async def async_wrapper(
            self: T_self,
            *args: Any,
            **kwargs: Any,
        ) -> Any:
            try:
                return await method(self, *args, **kwargs)
            except AppError:
                raise
            except Exception as exc:
                translated = _translate(self, exc)
                raise translated from exc

        return async_wrapper

    @functools.wraps(method)
    def sync_wrapper(
        self: T_self,
        *args: Any,
        **kwargs: Any,
    ) -> Any:
        try:
            return method(self, *args, **kwargs)
        except AppError:
            raise
        except Exception as exc:
            translated = _translate(self, exc)
            raise translated from exc

    return sync_wrapper


def _translate(self: Any, exc: Exception) -> AppError:
    """Look up ``self.translate_exception`` and call it.

    Defensive: if the call returns something that isn't an
    :class:`AppError`, raise :exc:`TypeError` so a buggy
    implementation fails loudly rather than re-raising a non-
    :class:`AppError` (which would silently break the typed-error
    contract).
    """
    method = getattr(self, "translate_exception", None)
    if method is None:
        msg = (
            f"@translate_at_seam used on {type(self).__name__}, "
            "which has no translate_exception method. Either "
            "subclass mgf.common.providers.ProviderABC or implement "
            "the method on the class."
        )
        raise TypeError(msg)

    result = method(exc)
    if not isinstance(result, AppError):
        msg = (
            f"{type(self).__name__}.translate_exception returned "
            f"{type(result).__name__} instead of AppError. The seam "
            "decorator requires a typed AppError return so the "
            "exception contract holds."
        )
        raise TypeError(msg)
    return result
