import json
from traceback import format_exception_only

import six
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
from requests import HTTPError


class AnymailError(Exception):
    """Base class for exceptions raised by Anymail

    Overrides __str__ to provide additional information about
    the ESP API call and response.
    """

    def __init__(self, *args, **kwargs):
        """
        Optional kwargs:
          email_message: the original EmailMessage being sent
          status_code: HTTP status code of response to ESP send call
          backend: the backend instance involved
          payload: data arg (*not* json-stringified) for the ESP send call
          response: requests.Response from the send call
          raised_from: original/wrapped Exception
          esp_name: what to call the ESP (read from backend if provided)
        """
        self.backend = kwargs.pop('backend', None)
        self.email_message = kwargs.pop('email_message', None)
        self.payload = kwargs.pop('payload', None)
        self.status_code = kwargs.pop('status_code', None)
        self.raised_from = kwargs.pop('raised_from', None)
        self.esp_name = kwargs.pop('esp_name',
                                   self.backend.esp_name if self.backend else None)
        if isinstance(self, HTTPError):
            # must leave response in kwargs for HTTPError
            self.response = kwargs.get('response', None)
        else:
            self.response = kwargs.pop('response', None)
        super(AnymailError, self).__init__(*args, **kwargs)

    def __str__(self):
        parts = [
            " ".join([str(arg) for arg in self.args]),
            self.describe_raised_from(),
            self.describe_send(),
            self.describe_response(),
        ]
        return "\n".join(filter(None, parts))

    def describe_send(self):
        """Return a string describing the ESP send in self.email_message, or None"""
        if self.email_message is None:
            return None
        description = "Sending a message"
        try:
            description += " to %s" % ','.join(self.email_message.to)
        except AttributeError:
            pass
        try:
            description += " from %s" % self.email_message.from_email
        except AttributeError:
            pass
        return description

    def describe_response(self):
        """Return a formatted string of self.status_code and response, or None"""
        if self.status_code is None:
            return None

        # Decode response.reason to text -- borrowed from requests.Response.raise_for_status:
        reason = self.response.reason
        if isinstance(reason, six.binary_type):
            try:
                reason = reason.decode('utf-8')
            except UnicodeDecodeError:
                reason = reason.decode('iso-8859-1')

        description = "%s API response %d: %s" % (self.esp_name or "ESP", self.status_code, reason)
        try:
            json_response = self.response.json()
            description += "\n" + json.dumps(json_response, indent=2)
        except (AttributeError, KeyError, ValueError):  # not JSON = ValueError
            try:
                description += " " + self.response.text
            except AttributeError:
                pass
        return description

    def describe_raised_from(self):
        """Return the original exception"""
        if self.raised_from is None:
            return None
        return ''.join(format_exception_only(type(self.raised_from), self.raised_from)).strip()


class AnymailAPIError(AnymailError):
    """Exception for unsuccessful response from ESP's API."""


class AnymailRequestsAPIError(AnymailAPIError, HTTPError):
    """Exception for unsuccessful response from a requests API."""

    def __init__(self, *args, **kwargs):
        super(AnymailRequestsAPIError, self).__init__(*args, **kwargs)
        if self.response is not None:
            self.status_code = self.response.status_code


class AnymailRecipientsRefused(AnymailError):
    """Exception for send where all recipients are invalid or rejected."""

    def __init__(self, message=None, *args, **kwargs):
        if message is None:
            message = "All message recipients were rejected or invalid"
        super(AnymailRecipientsRefused, self).__init__(message, *args, **kwargs)


class AnymailInvalidAddress(AnymailError, ValueError):
    """Exception when using an invalidly-formatted email address"""


class AnymailUnsupportedFeature(AnymailError, ValueError):
    """Exception for Anymail features that the ESP doesn't support.

    This is typically raised when attempting to send a Django EmailMessage that
    uses options or values you might expect to work, but that are silently
    ignored by or can't be communicated to the ESP's API.

    It's generally *not* raised for ESP-specific limitations, like the number
    of tags allowed on a message. (Anymail expects
    the ESP to return an API error for these where appropriate, and tries to
    avoid duplicating each ESP's validation logic locally.)

    """


class AnymailSerializationError(AnymailError, TypeError):
    """Exception for data that Anymail can't serialize for the ESP's API.

    This typically results from including something like a date or Decimal
    in your merge_vars.

    """
    # inherits from TypeError for compatibility with JSON serialization error

    def __init__(self, message=None, orig_err=None, *args, **kwargs):
        if message is None:
            # self.esp_name not set until super init, so duplicate logic to get esp_name
            backend = kwargs.get('backend', None)
            esp_name = kwargs.get('esp_name', backend.esp_name if backend else "the ESP")
            message = "Don't know how to send this data to %s. " \
                      "Try converting it to a string or number first." % esp_name
        if orig_err is not None:
            message += "\n%s" % str(orig_err)
        super(AnymailSerializationError, self).__init__(message, *args, **kwargs)


class AnymailCancelSend(AnymailError):
    """Pre-send signal receiver can raise to prevent message send"""


class AnymailWebhookValidationFailure(AnymailError, SuspiciousOperation):
    """Exception when a webhook cannot be validated.

    Django's SuspiciousOperation turns into
    an HTTP 400 error in production.
    """


class AnymailConfigurationError(ImproperlyConfigured):
    """Exception for Anymail configuration or installation issues"""
    # This deliberately doesn't inherit from AnymailError,
    # because we don't want it to be swallowed by backend fail_silently


class AnymailImproperlyInstalled(AnymailConfigurationError, ImportError):
    """Exception for Anymail missing package dependencies"""

    def __init__(self, missing_package, backend="<backend>"):
        message = "The %s package is required to use this ESP, but isn't installed.\n" \
                  "(Be sure to use `pip install django-anymail[%s]` " \
                  "with your desired ESPs.)" % (missing_package, backend)
        super(AnymailImproperlyInstalled, self).__init__(message)


# Warnings

class AnymailWarning(Warning):
    """Base warning for Anymail"""


class AnymailInsecureWebhookWarning(AnymailWarning):
    """Warns when webhook configured without any validation"""


class AnymailDeprecationWarning(AnymailWarning, DeprecationWarning):
    """Warning for deprecated Anymail features"""


# Helpers

class _LazyError(object):
    """An object that sits inert unless/until used, then raises an error"""
    def __init__(self, error):
        self._error = error

    def __call__(self, *args, **kwargs):
        raise self._error

    def __getattr__(self, item):
        raise self._error
