"""PRAW exception classes.

Includes two main exceptions: :class:`.RedditAPIException` for when something goes wrong
on the server side, and :class:`.ClientException` when something goes wrong on the
client side. Both of these classes extend :class:`.PRAWException`.

All other exceptions are subclassed from :class:`.ClientException`.

"""
from typing import List, Optional, Union
from warnings import warn

from .util import _deprecate_args


class PRAWException(Exception):
    """The base PRAW Exception that all other exception classes extend."""


class RedditErrorItem:
    """Represents a single error returned from Reddit's API."""

    @property
    def error_message(self) -> str:
        """Get the completed error message string."""
        error_str = self.error_type
        if self.message:
            error_str += f": {self.message!r}"
        if self.field:
            error_str += f" on field {self.field!r}"
        return error_str

    def __eq__(self, other: Union["RedditErrorItem", List[str]]):
        """Check for equality."""
        if isinstance(other, RedditErrorItem):
            return (self.error_type, self.message, self.field) == (
                other.error_type,
                other.message,
                other.field,
            )
        return super().__eq__(other)

    @_deprecate_args("error_type", "message", "field")
    def __init__(
        self,
        error_type: str,
        *,
        field: Optional[str] = None,
        message: Optional[str] = None,
    ):
        """Initialize a :class:`.RedditErrorItem` instance.

        :param error_type: The error type set on Reddit's end.
        :param field: The input field associated with the error, if available.
        :param message: The associated message for the error.

        """
        self.error_type = error_type
        self.message = message
        self.field = field

    def __repr__(self) -> str:
        """Return an object initialization representation of the instance."""
        return (
            f"{self.__class__.__name__}(error_type={self.error_type!r},"
            f" message={self.message!r}, field={self.field!r})"
        )

    def __str__(self):
        """Get the message returned from str(self)."""
        return self.error_message


class ClientException(PRAWException):
    """Indicate exceptions that don't involve interaction with Reddit's API."""


class DuplicateReplaceException(ClientException):
    """Indicate exceptions that involve the replacement of :class:`.MoreComments`."""

    def __init__(self):
        """Initialize a :class:`.DuplicateReplaceException` instance."""
        super().__init__(
            "A duplicate comment has been detected. Are you attempting to call"
            " 'replace_more_comments' more than once?"
        )


class InvalidFlairTemplateID(ClientException):
    """Indicate exceptions where an invalid flair template ID is given."""

    def __init__(self, template_id: str):
        """Initialize an :class:`.InvalidFlairTemplateID` instance."""
        super().__init__(
            f"The flair template ID '{template_id}' is invalid. If you are trying to"
            " create a flair, please use the 'add' method."
        )


class InvalidImplicitAuth(ClientException):
    """Indicate exceptions where an implicit auth type is used incorrectly."""

    def __init__(self):
        """Initialize an :class:`.InvalidImplicitAuth` instance."""
        super().__init__("Implicit authorization can only be used with installed apps.")


class InvalidURL(ClientException):
    """Indicate exceptions where an invalid URL is entered."""

    @_deprecate_args("url", "message")
    def __init__(self, url: str, *, message: str = "Invalid URL: {}"):
        """Initialize an :class:`.InvalidURL` instance.

        :param url: The invalid URL.
        :param message: The message to display. Must contain a format identifier (``{}``
            or ``{0}``) (default: ``"Invalid URL: {}"``).

        """
        super().__init__(message.format(url))


class MissingRequiredAttributeException(ClientException):
    """Indicate exceptions caused by not including a required attribute."""


class ReadOnlyException(ClientException):
    """Raised when a method call requires :attr:`.read_only` mode to be disabled."""


class TooLargeMediaException(ClientException):
    """Indicate exceptions from uploading media that's too large."""

    @_deprecate_args("maximum_size", "actual")
    def __init__(self, *, actual: int, maximum_size: int):
        """Initialize a :class:`.TooLargeMediaException` instance.

        :param actual: The actual size of the uploaded media.
        :param maximum_size: The maximum size of the uploaded media.

        """
        self.maximum_size = maximum_size
        self.actual = actual
        super().__init__(
            f"The media that you uploaded was too large (maximum size is {maximum_size}"
            f" bytes, uploaded {actual} bytes)"
        )


class WebSocketException(ClientException):
    """Indicate exceptions caused by use of WebSockets."""

    @property
    def original_exception(self) -> Exception:
        """Access the ``original_exception`` attribute (now deprecated)."""
        warn(
            "Accessing the attribute 'original_exception' is deprecated. Please rewrite"
            " your code in such a way that this attribute does not need to be used. It"
            " will be removed in PRAW 8.0.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self._original_exception

    @original_exception.setter
    def original_exception(self, value: Exception):
        self._original_exception = value

    @original_exception.deleter
    def original_exception(self):
        del self._original_exception

    def __init__(self, message: str, exception: Optional[Exception]):
        """Initialize a :class:`.WebSocketException` instance.

        :param message: The exception message.
        :param exception: The exception thrown by the websocket library.

            .. note::

                This parameter is deprecated. It will be removed in PRAW 8.0.

        """
        super().__init__(message)
        self._original_exception = exception


class MediaPostFailed(WebSocketException):
    """Indicate exceptions where media uploads failed.."""

    def __init__(self):
        """Initialize a :class:`.MediaPostFailed` instance."""
        super().__init__(
            "The attempted media upload action has failed. Possible causes include the"
            " corruption of media files. Check that the media file can be opened on"
            " your local machine.",
            None,
        )


class APIException(PRAWException):
    """Old class preserved for alias purposes.

    .. deprecated:: 7.0

        Class :class:`.APIException` has been deprecated in favor of
        :class:`.RedditAPIException`. This class will be removed in PRAW 8.0.

    """

    @staticmethod
    def parse_exception_list(exceptions: List[Union[RedditErrorItem, List[str]]]):
        """Covert an exception list into a :class:`.RedditErrorItem` list."""
        return [
            exception
            if isinstance(exception, RedditErrorItem)
            else RedditErrorItem(
                error_type=exception[0],
                field=exception[2] if bool(exception[2]) else "",
                message=exception[1] if bool(exception[1]) else "",
            )
            for exception in exceptions
        ]

    @property
    def error_type(self) -> str:
        """Get error_type.

        .. deprecated:: 7.0

            Accessing attributes through instances of :class:`.RedditAPIException` is
            deprecated. This behavior will be removed in PRAW 8.0. Check out the
            :ref:`PRAW 7 Migration tutorial <Exception_Handling>` on how to migrate code
            from this behavior.

        """
        return self._get_old_attr("error_type")

    @property
    def field(self) -> str:
        """Get field.

        .. deprecated:: 7.0

            Accessing attributes through instances of :class:`.RedditAPIException` is
            deprecated. This behavior will be removed in PRAW 8.0. Check out the
            :ref:`PRAW 7 Migration tutorial <Exception_Handling>` on how to migrate code
            from this behavior.

        """
        return self._get_old_attr("field")

    @property
    def message(self) -> str:
        """Get message.

        .. deprecated:: 7.0

            Accessing attributes through instances of :class:`.RedditAPIException` is
            deprecated. This behavior will be removed in PRAW 8.0. Check out the
            :ref:`PRAW 7 Migration tutorial <Exception_Handling>` on how to migrate code
            from this behavior.

        """
        return self._get_old_attr("message")

    def __init__(
        self,
        items: Union[List[Union[RedditErrorItem, List[str], str]], str],
        *optional_args: str,
    ):
        """Initialize a :class:`.RedditAPIException` instance.

        :param items: Either a list of instances of :class:`.RedditErrorItem` or a list
            containing lists of unformed errors.
        :param optional_args: Takes the second and third arguments that
            :class:`.APIException` used to take.

        """
        if isinstance(items, str):
            items = [[items, *optional_args]]
        elif isinstance(items, list) and isinstance(items[0], str):
            items = [items]
        self.items = self.parse_exception_list(items)
        super().__init__(*self.items)

    def _get_old_attr(self, attrname):
        warn(
            f"Accessing attribute '{attrname}' through APIException is deprecated."
            " This behavior will be removed in PRAW 8.0. Check out"
            " https://praw.readthedocs.io/en/latest/package_info/praw7_migration.html"
            " to learn how to migrate your code.",
            category=DeprecationWarning,
            stacklevel=3,
        )
        return getattr(self.items[0], attrname)


class RedditAPIException(APIException):
    """Container for error messages from Reddit's API."""
