########################################################################
# File name: errors.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program.  If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
"""
:mod:`~aioxmpp.errors` --- Exception classes
############################################

Exception classes mapping to XMPP stream errors
===============================================

.. autoclass:: StreamError

.. autoclass:: StreamErrorCondition

Exception classes mapping to XMPP stanza errors
===============================================

.. autoclass:: StanzaError

.. autoclass:: XMPPError

.. currentmodule:: aioxmpp

.. autoclass:: ErrorCondition

.. autoclass:: XMPPAuthError

.. autoclass:: XMPPModifyError

.. autoclass:: XMPPCancelError

.. autoclass:: XMPPWaitError

.. autoclass:: XMPPContinueError

.. currentmodule:: aioxmpp.errors

.. autoclass:: ErroneousStanza

Stream negotiation exceptions
=============================

.. autoclass:: StreamNegotiationFailure

.. autoclass:: SecurityNegotiationFailure

.. autoclass:: SASLUnavailable

.. autoclass:: TLSFailure

.. autoclass:: TLSUnavailable

I18N exceptions
===============

.. autoclass:: UserError

.. autoclass:: UserValueError

Other exceptions
================

.. autoclass:: MultiOSError

.. autoclass:: GatherError

"""
import enum
import gettext
import warnings

from . import xso, i18n, structs

from .utils import namespaces


def format_error_text(
        condition,
        text=None,
        application_defined_condition=None):
    error_tag = xso.tag_to_str(condition.value)
    if application_defined_condition is not None:
        error_tag += "/{}".format(
            xso.tag_to_str(application_defined_condition.TAG)
        )
    if text:
        error_tag += " ({!r})".format(text)
    return error_tag


class ErrorCondition(structs.CompatibilityMixin, xso.XSOEnumMixin, enum.Enum):
    """
    Enumeration to represent a :rfc:`6120` stanza error condition. Please
    see :rfc:`6120`, section 8.3.3, for the semantics of the individual
    conditions.

    .. versionadded:: 0.10

    .. attribute:: BAD_REQUEST
        :annotation: = namespaces.stanzas, "bad-request"

    .. attribute:: CONFLICT
        :annotation: = namespaces.stanzas, "conflict"

    .. attribute:: FEATURE_NOT_IMPLEMENTED
        :annotation: = namespaces.stanzas, "feature-not-implemented"

    .. attribute:: FORBIDDEN
        :annotation: = namespaces.stanzas, "forbidden"

    .. attribute:: GONE
        :annotation: = namespaces.stanzas, "gone"

        .. attribute:: xso_class

            .. attribute:: new_address

                The text content of the ``<gone/>`` element represtenting the
                URI at which the entity can now be found.

                May be :data:`None` if there is no such URI.

    .. attribute:: INTERNAL_SERVER_ERROR
        :annotation: = namespaces.stanzas, "internal-server-error"

    .. attribute:: ITEM_NOT_FOUND
        :annotation: = namespaces.stanzas, "item-not-found"

    .. attribute:: JID_MALFORMED
        :annotation: = namespaces.stanzas, "jid-malformed"

    .. attribute:: NOT_ACCEPTABLE
        :annotation: = namespaces.stanzas, "not-acceptable"

    .. attribute:: NOT_ALLOWED
        :annotation: = namespaces.stanzas, "not-allowed"

    .. attribute:: NOT_AUTHORIZED
        :annotation: = namespaces.stanzas, "not-authorized"

    .. attribute:: POLICY_VIOLATION
        :annotation: = namespaces.stanzas, "policy-violation"

    .. attribute:: RECIPIENT_UNAVAILABLE
        :annotation: = namespaces.stanzas, "recipient-unavailable"

    .. attribute:: REDIRECT
        :annotation: = namespaces.stanzas, "redirect"

        .. attribute:: xso_class

            .. attribute:: new_address

                The text content of the ``<redirect/>`` element represtenting
                the URI at which the entity can currently be found.

                May be :data:`None` if there is no such URI.

    .. attribute:: REGISTRATION_REQUIRED
        :annotation: = namespaces.stanzas, "registration-required"

    .. attribute:: REMOTE_SERVER_NOT_FOUND
        :annotation: = namespaces.stanzas, "remote-server-not-found"

    .. attribute:: REMOTE_SERVER_TIMEOUT
        :annotation: = namespaces.stanzas, "remote-server-timeout"

    .. attribute:: RESOURCE_CONSTRAINT
        :annotation: = namespaces.stanzas, "resource-constraint"

    .. attribute:: SERVICE_UNAVAILABLE
        :annotation: = namespaces.stanzas, "service-unavailable"

    .. attribute:: SUBSCRIPTION_REQUIRED
        :annotation: = namespaces.stanzas, "subscription-required"

    .. attribute:: UNDEFINED_CONDITION
        :annotation: = namespaces.stanzas, "undefined-condition"

    .. attribute:: UNEXPECTED_REQUEST
        :annotation: = namespaces.stanzas, "unexpected-request"

    """

    BAD_REQUEST = (namespaces.stanzas, "bad-request")
    CONFLICT = (namespaces.stanzas, "conflict")
    FEATURE_NOT_IMPLEMENTED = (namespaces.stanzas, "feature-not-implemented")
    FORBIDDEN = (namespaces.stanzas, "forbidden")
    GONE = (namespaces.stanzas, "gone")
    INTERNAL_SERVER_ERROR = (namespaces.stanzas, "internal-server-error")
    ITEM_NOT_FOUND = (namespaces.stanzas, "item-not-found")
    JID_MALFORMED = (namespaces.stanzas, "jid-malformed")
    NOT_ACCEPTABLE = (namespaces.stanzas, "not-acceptable")
    NOT_ALLOWED = (namespaces.stanzas, "not-allowed")
    NOT_AUTHORIZED = (namespaces.stanzas, "not-authorized")
    POLICY_VIOLATION = (namespaces.stanzas, "policy-violation")
    RECIPIENT_UNAVAILABLE = (namespaces.stanzas, "recipient-unavailable")
    REDIRECT = (namespaces.stanzas, "redirect")
    REGISTRATION_REQUIRED = (namespaces.stanzas, "registration-required")
    REMOTE_SERVER_NOT_FOUND = (namespaces.stanzas, "remote-server-not-found")
    REMOTE_SERVER_TIMEOUT = (namespaces.stanzas, "remote-server-timeout")
    RESOURCE_CONSTRAINT = (namespaces.stanzas, "resource-constraint")
    SERVICE_UNAVAILABLE = (namespaces.stanzas, "service-unavailable")
    SUBSCRIPTION_REQUIRED = (namespaces.stanzas, "subscription-required")
    UNDEFINED_CONDITION = (namespaces.stanzas, "undefined-condition")
    UNEXPECTED_REQUEST = (namespaces.stanzas, "unexpected-request")


ErrorCondition.GONE.xso_class.new_address = xso.Text()
ErrorCondition.REDIRECT.xso_class.new_address = xso.Text()


class StreamErrorCondition(structs.CompatibilityMixin,
                           xso.XSOEnumMixin,
                           enum.Enum):
    """
    Enumeration to represent a :rfc:`6120` stream  error condition. Please
    see :rfc:`6120`, section 4.9.3, for the semantics of the individual
    conditions.

    .. versionadded:: 0.10

    .. attribute:: BAD_FORMAT
        :annotation: = (namespaces.streams, "bad-format")

    .. attribute:: BAD_NAMESPACE_PREFIX
        :annotation: = (namespaces.streams, "bad-namespace-prefix")

    .. attribute:: CONFLICT
        :annotation: = (namespaces.streams, "conflict")

    .. attribute:: CONNECTION_TIMEOUT
        :annotation: = (namespaces.streams, "connection-timeout")

    .. attribute:: HOST_GONE
        :annotation: = (namespaces.streams, "host-gone")

    .. attribute:: HOST_UNKNOWN
        :annotation: = (namespaces.streams, "host-unknown")

    .. attribute:: IMPROPER_ADDRESSING
        :annotation: = (namespaces.streams, "improper-addressing")

    .. attribute:: INTERNAL_SERVER_ERROR
        :annotation: = (namespaces.streams, "internal-server-error")

    .. attribute:: INVALID_FROM
        :annotation: = (namespaces.streams, "invalid-from")

    .. attribute:: INVALID_NAMESPACE
        :annotation: = (namespaces.streams, "invalid-namespace")

    .. attribute:: INVALID_XML
        :annotation: = (namespaces.streams, "invalid-xml")

    .. attribute:: NOT_AUTHORIZED
        :annotation: = (namespaces.streams, "not-authorized")

    .. attribute:: NOT_WELL_FORMED
        :annotation: = (namespaces.streams, "not-well-formed")

    .. attribute:: POLICY_VIOLATION
        :annotation: = (namespaces.streams, "policy-violation")

    .. attribute:: REMOTE_CONNECTION_FAILED
        :annotation: = (namespaces.streams, "remote-connection-failed")

    .. attribute:: RESET
        :annotation: = (namespaces.streams, "reset")

    .. attribute:: RESOURCE_CONSTRAINT
        :annotation: = (namespaces.streams, "resource-constraint")

    .. attribute:: RESTRICTED_XML
        :annotation: = (namespaces.streams, "restricted-xml")

    .. attribute:: SEE_OTHER_HOST
        :annotation: = (namespaces.streams, "see-other-host")

    .. attribute:: SYSTEM_SHUTDOWN
        :annotation: = (namespaces.streams, "system-shutdown")

    .. attribute:: UNDEFINED_CONDITION
        :annotation: = (namespaces.streams, "undefined-condition")

    .. attribute:: UNSUPPORTED_ENCODING
        :annotation: = (namespaces.streams, "unsupported-encoding")

    .. attribute:: UNSUPPORTED_FEATURE
        :annotation: = (namespaces.streams, "unsupported-feature")

    .. attribute:: UNSUPPORTED_STANZA_TYPE
        :annotation: = (namespaces.streams, "unsupported-stanza-type")

    .. attribute:: UNSUPPORTED_VERSION
        :annotation: = (namespaces.streams, "unsupported-version")

    """

    BAD_FORMAT = (namespaces.streams, "bad-format")
    BAD_NAMESPACE_PREFIX = (namespaces.streams, "bad-namespace-prefix")
    CONFLICT = (namespaces.streams, "conflict")
    CONNECTION_TIMEOUT = (namespaces.streams, "connection-timeout")
    HOST_GONE = (namespaces.streams, "host-gone")
    HOST_UNKNOWN = (namespaces.streams, "host-unknown")
    IMPROPER_ADDRESSING = (namespaces.streams, "improper-addressing")
    INTERNAL_SERVER_ERROR = (namespaces.streams, "internal-server-error")
    INVALID_FROM = (namespaces.streams, "invalid-from")
    INVALID_NAMESPACE = (namespaces.streams, "invalid-namespace")
    INVALID_XML = (namespaces.streams, "invalid-xml")
    NOT_AUTHORIZED = (namespaces.streams, "not-authorized")
    NOT_WELL_FORMED = (namespaces.streams, "not-well-formed")
    POLICY_VIOLATION = (namespaces.streams, "policy-violation")
    REMOTE_CONNECTION_FAILED = (namespaces.streams, "remote-connection-failed")
    RESET = (namespaces.streams, "reset")
    RESOURCE_CONSTRAINT = (namespaces.streams, "resource-constraint")
    RESTRICTED_XML = (namespaces.streams, "restricted-xml")
    SEE_OTHER_HOST = (namespaces.streams, "see-other-host")
    SYSTEM_SHUTDOWN = (namespaces.streams, "system-shutdown")
    UNDEFINED_CONDITION = (namespaces.streams, "undefined-condition")
    UNSUPPORTED_ENCODING = (namespaces.streams, "unsupported-encoding")
    UNSUPPORTED_FEATURE = (namespaces.streams, "unsupported-feature")
    UNSUPPORTED_STANZA_TYPE = (namespaces.streams, "unsupported-stanza-type")
    UNSUPPORTED_VERSION = (namespaces.streams, "unsupported-version")


StreamErrorCondition.SEE_OTHER_HOST.xso_class.new_address = xso.Text()


class StreamError(ConnectionError):
    def __init__(self, condition, text=None):
        if not isinstance(condition, StreamErrorCondition):
            condition = StreamErrorCondition(condition)
            warnings.warn(
                "as of aioxmpp 1.0, stream error conditions must be members of "
                "the aioxmpp.errors.StreamErrorCondition enumeration",
                DeprecationWarning,
                stacklevel=2,
            )

        super().__init__("stream error: {}".format(
            format_error_text(condition, text))
        )
        self.condition = condition
        self.text = text


class StanzaError(Exception):
    pass


class XMPPError(StanzaError):
    """
    Exception representing an error defined in the XMPP protocol.

    :param condition: The :rfc:`6120` defined error condition as enumeration
        member or :class:`aioxmpp.xso.XSO`
    :type condition: :class:`aioxmpp.ErrorCondition` or :class:`aioxmpp.xso.XSO`
    :param text: Optional human-readable text explaining the error
    :type text: :class:`str`
    :param application_defined_condition: Object describing the error in more
        detail
    :type application_defined_condition: :class:`aioxmpp.xso.XSO`

    .. versionchanged:: 0.10

        As of 0.10, `condition` should either be a
        :class:`aioxmpp.ErrorCondition` enumeration member or an XSO
        representing one of the error conditions.

        For compatibility, namespace-localpart tuples indicating the tag of
        the defined error condition are still accepted.

    .. deprecated:: 0.10

        Starting with aioxmpp 1.0, namespace-localpart tuples will not be
        accepted anymore. See the changelog for notes on the transition.

    .. attribute:: condition_obj

        The :class:`aioxmpp.XSO` which represents the error condition.

        .. versionadded:: 0.10

    .. autoattribute:: condition

    .. attribute:: text

        Optional human-readable text describing the error further.

        This is :data:`None` if the text is omitted.

    .. attribute:: application_defined_condition

        Optional :class:`aioxmpp.XSO` which further defines the error
        condition.

    Relevant subclasses:

    .. autosummary::

       aioxmpp.XMPPAuthError
       aioxmpp.XMPPModifyError
       aioxmpp.XMPPCancelError
       aioxmpp.XMPPContinueError
       aioxmpp.XMPPWaitError

    """

    TYPE = structs.ErrorType.CANCEL

    def __init__(self,
                 condition,
                 text=None,
                 application_defined_condition=None):
        if not isinstance(condition, (ErrorCondition, xso.XSO)):
            condition = ErrorCondition(condition)
            warnings.warn(
                "as of aioxmpp 1.0, error conditions must be members of the "
                "aioxmpp.ErrorCondition enumeration",
                DeprecationWarning,
                stacklevel=2,
            )

        super().__init__(format_error_text(
            condition.enum_member,
            text=text,
            application_defined_condition=application_defined_condition))
        self.condition_obj = condition.to_xso()
        self.text = text
        self.application_defined_condition = application_defined_condition

    @property
    def condition(self):
        """
        :class:`aioxmpp.ErrorCondition` enumeration member representing the
        error condition.
        """

        return self.condition_obj.enum_member


class XMPPWarning(XMPPError, UserWarning):
    TYPE = structs.ErrorType.CONTINUE


class XMPPAuthError(XMPPError, PermissionError):
    TYPE = structs.ErrorType.AUTH


class XMPPModifyError(XMPPError, ValueError):
    TYPE = structs.ErrorType.MODIFY


class XMPPCancelError(XMPPError):
    TYPE = structs.ErrorType.CANCEL


class XMPPWaitError(XMPPError):
    TYPE = structs.ErrorType.WAIT


class XMPPContinueError(XMPPWarning):
    TYPE = structs.ErrorType.CONTINUE


class ErroneousStanza(StanzaError):
    """
    This exception is thrown into listeners for IQ responses by
    :class:`aioxmpp.stream.StanzaStream` if a response for an IQ was received,
    but could not be decoded (due to malformed or unsupported payload).

    .. attribute:: partial_obj

       Contains the partially decoded stanza XSO. Do not rely on any members
       except those representing XML attributes (:attr:`~.StanzaBase.to`,
       :attr:`~.StanzaBase.from_`, :attr:`~.StanzaBase.type_`).

    """

    def __init__(self, partial_obj):
        super().__init__("erroneous stanza received: {!r}".format(
            partial_obj))
        self.partial_obj = partial_obj


class StreamNegotiationFailure(ConnectionError):
    pass


class SecurityNegotiationFailure(StreamNegotiationFailure):
    def __init__(self, xmpp_error,
                 kind="Security negotiation failure",
                 text=None):
        msg = "{}: {}".format(kind, xmpp_error)
        if text:
            msg += " ('{}')".format(text)
        super().__init__(msg)
        self.xmpp_error = xmpp_error
        self.text = text


class SASLUnavailable(SecurityNegotiationFailure):
    # we use this to tell the Client that SASL has not been available at all,
    # or that we could not agree on mechansims.
    # it might be helpful to notify the peer about this before dying.
    pass


class TLSFailure(SecurityNegotiationFailure):
    def __init__(self, xmpp_error, text=None):
        super().__init__(xmpp_error, text=text, kind="TLS failure")


class TLSUnavailable(TLSFailure):
    pass


class UserError(Exception):
    """
    An exception subclass, which should be used as a mix-in.

    It is intended to be used for exceptions which may be user-facing, such as
    connection errors, value validation issues and the like.

    `localizable_string` must be a :class:`.i18n.LocalizableString`
    instance. The `args` and `kwargs` will be passed to
    :class:`.LocalizableString.localize` when either :func:`str` is called on
    the :class:`UserError` or :meth:`localize` is called.

    The :func:`str` is created using the default
    :class:`~.i18n.LocalizingFormatter` and a :class:`gettext.NullTranslations`
    instance. The point in time at which the default localizing formatter is
    created is unspecified.

    .. automethod:: localize

    """

    DEFAULT_FORMATTER = i18n.LocalizingFormatter()
    DEFAULT_TRANSLATIONS = gettext.NullTranslations()

    def __init__(self, localizable_string, *args, **kwargs):
        super().__init__()
        self._str = localizable_string.localize(
            self.DEFAULT_FORMATTER,
            self.DEFAULT_TRANSLATIONS,
            *args, **kwargs)
        self.localizable_string = localizable_string
        self.args = args
        self.kwargs = kwargs

    def __str__(self):
        return str(self._str)

    def localize(self, formatter, translator):
        """
        Return a localized version of the `localizable_string` passed to the
        consturctor. It is formatted using the `formatter` with the `args` and
        `kwargs` passed to the constructor of :class:`UserError`.
        """
        return self.localizable_string.localize(
            formatter,
            translator,
            *self.args,
            **self.kwargs
        )


class UserValueError(UserError, ValueError):
    """
    This is a :class:`ValueError` with :class:`UserError` mixed in.
    """


class MultiOSError(OSError):
    """
    Describe an error situation which has been caused by the sequential
    occurence of multiple other `exceptions`.

    The `message` shall be descriptive and will be prepended to a concatenation
    of the error messages of the given `exceptions`.
    """

    def __init__(self, message, exceptions):
        flattened_exceptions = []
        for exc in exceptions:
            if hasattr(exc, "exceptions"):
                flattened_exceptions.extend(exc.exceptions)
            else:
                flattened_exceptions.append(exc)

        super().__init__(
            "{}: multiple errors: {}".format(
                message,
                ", ".join(map(str, flattened_exceptions))
            )
        )
        self.exceptions = flattened_exceptions


class GatherError(RuntimeError):
    """
    Describe an error situation which has been caused by the occurence
    of multiple other `exceptions`.

    The `message` shall be descriptive and will be prepended to a concatenation
    of the error messages of the given `exceptions`.
    """

    def __init__(self, message, exceptions):
        flattened_exceptions = []
        for exc in exceptions:
            if hasattr(exc, "exceptions"):
                flattened_exceptions.extend(exc.exceptions)
            else:
                flattened_exceptions.append(exc)

        super().__init__(
            "{}: multiple errors: {}".format(
                message,
                ", ".join(map(str, flattened_exceptions))
            )
        )
        self.exceptions = flattened_exceptions
