import json
import logging
import typing

from public import public


if typing.TYPE_CHECKING:
    from urllib3.response import HTTPHeaderDict  # type: ignore[attr-defined]

    from .rest import RESTResponse

logger = logging.getLogger(__name__)


class _SnowflakePythonError(Exception):
    """The base exception class for all Snowflake Python related Errors."""

    def __init__(self, *args: typing.Any) -> None:
        super().__init__(*args)


class _OpenAPIError(_SnowflakePythonError):
    """The abstract class for all APIErrors such as TypeErrors, ValueErrors, and API Error Codes."""

    def __init__(self, *args: typing.Any) -> None:
        super().__init__(*args)
        class_name = self.__class__.__name__
        logger.error(
            "An exception of type %s was created, it's probably going to be raised.",
            class_name,
        )
        logger.debug(
            "This exception has the following args: %r.",
            args,
        )


class _APITypeError(_OpenAPIError, TypeError):
    """Raises an exception for TypeErrors.

    Args:
        msg (str): the exception message

    Keyword Args:
        path_to_item (list): a list of keys an indices to get to the
                             current_item
                             None if unset
        valid_classes (tuple): the primitive classes that current item
                               should be an instance of
                               None if unset
        key_type (bool): False if our value is a value in a dict
                         True if it is a key in a dict
                         False if our item is an item in a list
                         None if unset
    """

    def __init__(
        self,
        msg: str,
        path_to_item: typing.Optional[typing.List[typing.Union[str, int]]] = None,
        valid_classes: typing.Optional[typing.Tuple[typing.Type[typing.Any], ...]] = None,
        key_type: typing.Optional[bool] = None,
    ) -> None:
        self.path_to_item = path_to_item
        self.valid_classes = valid_classes
        self.key_type = key_type
        full_msg = msg
        if path_to_item:
            full_msg = f"{msg} at {_render_path(path_to_item)}"
        super().__init__(full_msg)


class _APIValueError(_OpenAPIError, ValueError):
    """Raises an exception for ValueErrors.

    Args:
        msg (str): the exception message.

    Keyword Args:
        path_to_item (list) the path to the exception in the
            received_data dict. None if unset
    """

    def __init__(
        self,
        msg: str,
        path_to_item: typing.Optional[typing.List[typing.Union[str, int]]] = None,
    ) -> None:
        self.path_to_item = path_to_item
        full_msg = msg
        if path_to_item:
            full_msg = f"{msg} at {_render_path(path_to_item)}"
        super().__init__(full_msg)


@public
class APIError(_OpenAPIError):
    """Raised when there is an exception with any of the API Error Codes."""

    def __init__(
        self,
        status: typing.Optional[int] = None,
        reason: typing.Optional[str] = None,
        http_resp: typing.Optional["RESTResponse"] = None,
        request_id: typing.Optional[str] = None,
    ) -> None:
        self.status = status
        self.reason = reason
        self.body: typing.Optional[typing.Union[bytes, typing.Any]] = None
        self.headers: typing.Optional[HTTPHeaderDict] = None
        self.request_id = request_id
        if http_resp:
            self.status = http_resp.status
            self.reason = http_resp.reason
            self.body = http_resp.data
            self.headers = http_resp.getheaders()
            # Let's try to find request id if we can
            try:
                self.request_id = json.loads(self.body).get("request_id")
            except Exception:
                logger.error("Failed to extract request_id from the response body for an Error")
                logger.debug("The response body is: %r", self.body)
        super().__init__(str(self))

    def __str__(self) -> str:
        """Provide a custom error message for the exception."""
        error_message = f"({self.status})\nReason: {self.reason}\n"
        if self.headers:
            error_message += f"HTTP response headers: {self.headers}\n"

        if self.body:
            if isinstance(self.body, bytes):
                body = self.body.decode()
            else:
                body = self.body
            error_message += f"HTTP response body: {body}\n"

        return error_message


@public
class NotFoundError(APIError):
    """Raised when we encounter an HTTP error code of 404."""

    def __init__(
        self,
        status: typing.Optional[int] = None,
        reason: typing.Optional[str] = None,
        http_resp: typing.Optional["RESTResponse"] = None,
    ) -> None:
        super().__init__(status, reason, http_resp)


@public
class UnauthorizedError(APIError):
    """Raised when we encounter an HTTP error code of 401."""

    def __init__(
        self,
        status: typing.Optional[int] = None,
        reason: typing.Optional[str] = None,
        http_resp: typing.Optional["RESTResponse"] = None,
    ) -> None:
        super().__init__(status, reason, http_resp)


@public
class ForbiddenError(APIError):
    """Raised when we encounter an HTTP error code of 403."""

    def __init__(
        self,
        status: typing.Optional[int] = None,
        reason: typing.Optional[str] = None,
        http_resp: typing.Optional["RESTResponse"] = None,
    ) -> None:
        super().__init__(status, reason, http_resp)


@public
class ServerError(APIError):
    """Raised when we encounter an HTTP error code of 5NN."""

    def __init__(
        self,
        status: typing.Optional[int] = None,
        reason: typing.Optional[str] = None,
        http_resp: typing.Optional["RESTResponse"] = None,
    ) -> None:
        super().__init__(status, reason, http_resp)


@public
class ConflictError(APIError):
    """Raised when we encounter an HTTP error code of 409."""

    def __init__(
        self,
        status: typing.Optional[int] = None,
        reason: typing.Optional[str] = None,
        http_resp: typing.Optional["RESTResponse"] = None,
    ) -> None:
        super().__init__(status, reason, http_resp)


def _render_path(
    path_to_item: typing.Optional[typing.List[typing.Union[str, int]]],
) -> str:
    """Return a string representation of a path."""
    result = ""
    if path_to_item:
        for pth in path_to_item:
            if isinstance(pth, int):
                result += f"[{pth}]"
            else:
                result += f"['{pth}']"
    return result


@public
class InvalidActionError(_SnowflakePythonError):
    """Raised when the user makes an invalid action such as invalid parameters or when the API response is invalid.

    Base class for all errors such as InvalidArgumentsError, InvalidResponseError, etc.
    """

    def __init__(self, *args: typing.Any) -> None:
        super().__init__(*args)


@public
class InvalidResponseError(InvalidActionError):
    """Raised when the api response is invalid."""

    def __init__(
        self,
        reason: typing.Optional[str] = None
    ) -> None:
        super().__init__(reason)
        self.reason = reason


@public
class InvalidArgumentsError(InvalidActionError):
    """Raised when function args are invalid."""

    def __init__(
        self,
        reason: typing.Optional[str] = None
    ) -> None:
        super().__init__(reason)
        self.reason = reason


@public
class InvalidResultError(InvalidActionError):
    """Raised when function result is invalid."""

    def __init__(
        self,
        reason: typing.Optional[str] = None
    ) -> None:
        super().__init__(reason)
        self.reason = reason


@public
class InvalidOperationError(InvalidActionError):
    """Raised when user tries to make an invalid operation."""

    def __init__(
        self,
        reason: typing.Optional[str] = None
    ) -> None:
        super().__init__(reason)
        self.reason = reason


@public
class RetryTimeoutError(_SnowflakePythonError):
    """Raised when a request times out after any amount of retries."""

    def __init__(
            self,
            reason: typing.Optional[str] = None
    ) -> None:
        super().__init__(reason)
        self.reason = reason
