from __future__ import annotations

__all__ = ["ConnectError", "ErrorDetail"]


from typing import TYPE_CHECKING, TypeVar, overload

from protobuf import Message, Registry
from protobuf.wkt import Any

if TYPE_CHECKING:
    from collections.abc import Iterable, Sequence

    from protobuf import DescMessage

    from .code import Code

T = TypeVar("T", bound=Message)


class ErrorDetail:
    """A self-describing Protobuf message attached to a [ConnectError][].

    Error details are sent over the network to clients, which can then work with
    strongly-typed data rather than trying to parse a complex error message. For
    example, you might use details to send a localized error message or retry
    parameters to a client.
    """

    def __init__(self, message: Message) -> None:
        if isinstance(message, Any):
            self._message = None
            self._any = message
            return
        self._message = message
        self._any = Any.pack(message)

    @property
    def type_name(self) -> str:
        """The fully-qualified name of the details Protobuf message (for example, acme.foo.v1.FooDetail)."""
        return self._any.type_url.removeprefix("type.googleapis.com/")

    @property
    def message_bytes(self) -> bytes:
        """The Protobuf message serialized as bytes."""
        return self._any.value

    @overload
    def value(self) -> Message | None: ...

    @overload
    def value(self, desc: DescMessage, /) -> Message | None: ...

    @overload
    def value(self, typ: type[T], /) -> T | None: ...

    @overload
    def value(self, registry: Registry, /) -> Message | None: ...

    def value(
        self, desc_or_registry: Registry | DescMessage | type[Message] | None = None
    ) -> Message | None:
        """The details message as a Protobuf message, or None if it cannot be deserialized."""
        if self._message:
            return self._message
        if not desc_or_registry:
            return None
        if isinstance(desc_or_registry, Registry):
            desc = desc_or_registry.message(self.type_name)
            if not desc:
                return None
        else:
            desc = desc_or_registry
        return self._any.unpack(desc)


class ConnectError(Exception):
    """An exception in a Connect RPC.

    If a server raises a ConnectError, the same exception content will be
    raised on the client as well. Errors surfacing on the client side such as
    timeouts will also be raised as a ConnectError with an appropriate
    [Code][].
    """

    def __init__(
        self, code: Code, message: str, details: Iterable[Message | ErrorDetail] = ()
    ) -> None:
        """
        Creates a new Connect error.

        Args:
            code: The error code.
            message: The error message.
            details: Additional details about the error.
        """
        super().__init__(message)
        self._code = code
        self._message = message

        self._details = (
            [m if isinstance(m, ErrorDetail) else ErrorDetail(m) for m in details]
            if details
            else ()
        )

    @property
    def code(self) -> Code:
        return self._code

    @property
    def message(self) -> str:
        return self._message

    @property
    def details(self) -> Sequence[ErrorDetail]:
        return self._details
