"""
    Exceptions raised and handled in Conan server.
    These exceptions are mapped between server (as an HTTP response) and client
    through the REST API. When an error happens in server its translated to an HTTP
    error code that its sent to client. Client reads the server code and raise the
    matching exception.

    see return_plugin.py

"""
from contextlib import contextmanager
from subprocess import CalledProcessError

from conans.util.env_reader import get_env
from conans.util.files import decode_text


class CalledProcessErrorWithStderr(CalledProcessError):
    def __str__(self):
        ret = super(CalledProcessErrorWithStderr, self).__str__()
        if self.output:
            ret += "\n" + decode_text(self.output)
        return ret


@contextmanager
def conanfile_exception_formatter(conanfile_name, func_name):
    """
    Decorator to throw an exception formatted with the line of the conanfile where the error ocurrs.
    :param reference: Reference of the conanfile
    :return:
    """
    try:
        yield
    # TODO: Move ConanInvalidSystemRequirements, ConanInvalidConfiguration from here?
    except ConanInvalidSystemRequirements as exc:
        msg = "{}: Invalid system requirements: {}".format(conanfile_name, exc)
        raise ConanInvalidSystemRequirements(msg)
    except ConanInvalidConfiguration as exc:
        msg = "{}: Invalid configuration: {}".format(conanfile_name, exc)
        raise ConanInvalidConfiguration(msg)
    except Exception as exc:
        msg = _format_conanfile_exception(conanfile_name, func_name, exc)
        raise ConanExceptionInUserConanfileMethod(msg)


def _format_conanfile_exception(scope, method, exception):
    """
    It will iterate the traceback lines, when it finds that the source code is inside the users
    conanfile it "start recording" the messages, when the trace exits the conanfile we return
    the traces.
    """
    import sys
    import traceback
    if get_env("CONAN_VERBOSE_TRACEBACK", False):
        return traceback.format_exc()
    try:
        conanfile_reached = False
        tb = sys.exc_info()[2]
        index = 0
        content_lines = []

        while True:  # If out of index will raise and will be captured later
            # 40 levels of nested functions max, get the latest
            filepath, line, name, contents = traceback.extract_tb(tb, 40)[index]
            if "conanfile.py" not in filepath:  # Avoid show trace from internal conan source code
                if conanfile_reached:  # The error goes to internal code, exit print
                    break
            else:
                if not conanfile_reached:  # First line
                    msg = "%s: Error in %s() method" % (scope, method)
                    msg += ", line %d\n\t%s" % (line, contents)
                else:
                    msg = ("while calling '%s', line %d\n\t%s" % (name, line, contents)
                           if line else "\n\t%s" % contents)
                content_lines.append(msg)
                conanfile_reached = True
            index += 1
    except Exception:
        pass
    ret = "\n".join(content_lines)
    ret += "\n\t%s: %s" % (exception.__class__.__name__, str(exception))
    return ret


class ConanException(Exception):
    """
         Generic conans exception
    """
    def __init__(self, *args, **kwargs):
        self.info = None
        self.remote = kwargs.pop("remote", None)
        super(ConanException, self).__init__(*args, **kwargs)

    def remote_message(self):
        if self.remote:
            return " [Remote: {}]".format(self.remote.name)
        return ""

    def __str__(self):
        from conans.util.files import exception_message_safe
        msg = super(ConanException, self).__str__()
        if self.remote:
            return "{}.{}".format(exception_message_safe(msg), self.remote_message())

        return exception_message_safe(msg)


class ConanV2Exception(ConanException):
    def __str__(self):
        msg = super(ConanV2Exception, self).__str__()
        # TODO: Add a link to a public webpage with Conan roadmap to v2
        return "Conan v2 incompatible: {}".format(msg)


class OnlyV2Available(ConanException):

    def __init__(self, remote_url):
        msg = "The remote at '%s' only works with revisions enabled. " \
              "Set CONAN_REVISIONS_ENABLED=1 " \
              "or set 'general.revisions_enabled = 1' at the 'conan.conf'" % remote_url
        super(OnlyV2Available, self).__init__(msg)


class NoRestV2Available(ConanException):
    pass


class NoRemoteAvailable(ConanException):
    """ No default remote configured or the specified remote do not exists
    """
    pass


class InvalidNameException(ConanException):
    pass


class ConanConnectionError(ConanException):
    pass


class ConanOutdatedClient(ConanException):
    pass


class ConanExceptionInUserConanfileMethod(ConanException):
    pass


class ConanInvalidSystemRequirements(ConanException):
    pass


class ConanInvalidConfiguration(ConanExceptionInUserConanfileMethod):
    pass


class ConanMigrationError(ConanException):
    pass


# Remote exceptions #
class InternalErrorException(ConanException):
    """
         Generic 500 error
    """
    pass


class RequestErrorException(ConanException):
    """
         Generic 400 error
    """
    pass


class AuthenticationException(ConanException):  # 401
    """
        401 error
    """
    pass


class ForbiddenException(ConanException):  # 403
    """
        403 error
    """
    pass


class NotFoundException(ConanException):  # 404
    """
        404 error
    """

    def __init__(self, *args, **kwargs):
        self.remote = kwargs.pop("remote", None)
        super(NotFoundException, self).__init__(*args, **kwargs)


class RecipeNotFoundException(NotFoundException):

    def __init__(self, ref, remote=None, print_rev=False):
        from conans.model.ref import ConanFileReference
        assert isinstance(ref, ConanFileReference), "RecipeNotFoundException requires a " \
                                                    "ConanFileReference"
        self.ref = ref
        self.print_rev = print_rev
        super(RecipeNotFoundException, self).__init__(remote=remote)

    def __str__(self):
        tmp = self.ref.full_str() if self.print_rev else str(self.ref)
        return "Recipe not found: '{}'".format(tmp, self.remote_message())


class PackageNotFoundException(NotFoundException):

    def __init__(self, pref, remote=None, print_rev=False):
        from conans.model.ref import PackageReference
        assert isinstance(pref, PackageReference), "PackageNotFoundException requires a " \
                                                   "PackageReference"
        self.pref = pref
        self.print_rev = print_rev

        super(PackageNotFoundException, self).__init__(remote=remote)

    def __str__(self):
        tmp = self.pref.full_str() if self.print_rev else str(self.pref)
        return "Binary package not found: '{}'{}".format(tmp, self.remote_message())


class UserInterfaceErrorException(RequestErrorException):
    """
        420 error
    """
    pass


EXCEPTION_CODE_MAPPING = {InternalErrorException: 500,
                          RequestErrorException: 400,
                          AuthenticationException: 401,
                          ForbiddenException: 403,
                          NotFoundException: 404,
                          RecipeNotFoundException: 404,
                          PackageNotFoundException: 404,
                          UserInterfaceErrorException: 420}
