"""Exceptions raised by Apps."""
from functools import wraps
from typing import Callable, List, Union, Any, TypeVar, Optional
from types import TracebackType
import logging
from tblib import Traceback

from six import reraise

from parsl.data_provider.files import File

logger = logging.getLogger(__name__)


class ParslError(Exception):
    """Base class for all exceptions.

    Only to be invoked when a more specific error is not available.
    """


class AppException(ParslError):
    """An error raised during execution of an app.

    What this exception contains depends entirely on context
    """


class AppBadFormatting(ParslError):
    """An error raised during formatting of a bash function.
    """


class BashExitFailure(AppException):
    """A non-zero exit code returned from a @bash_app

    Contains:
    app name (str)
    exitcode (int)
    """

    def __init__(self, app_name: str, exitcode: int) -> None:
        self.app_name = app_name
        self.exitcode = exitcode

    def __str__(self) -> str:
        return f"bash_app {self.app_name} failed with unix exit code {self.exitcode}"


class AppTimeout(AppException):
    """An error raised during execution of an app when it exceeds its allotted walltime.
    """


class BashAppNoReturn(AppException):
    """Bash app returned no string.

    Contains:
    reason(string)
    """

    def __init__(self, reason: str) -> None:
        super().__init__(reason)
        self.reason = reason


class MissingOutputs(ParslError):
    """Error raised at the end of app execution due to missing output files.

    Contains:
    reason(string)
    outputs(List of strings/files..)
    """

    def __init__(self, reason: str, outputs: List[Union[str, File]]) -> None:
        super().__init__(reason, outputs)
        self.reason = reason
        self.outputs = outputs

    def __repr__(self) -> str:
        return "Missing Outputs: {0}, Reason:{1}".format(self.outputs, self.reason)


class BadStdStreamFile(ParslError):
    """Error raised due to bad filepaths specified for STDOUT/ STDERR.

    Contains:
       reason(string)
       exception object
    """

    def __init__(self, reason: str, exception: Exception) -> None:
        super().__init__(reason, exception)
        self._reason = reason
        self._exception = exception

    def __repr__(self) -> str:
        return "Bad Stream File: {} Exception: {}".format(self._reason, self._exception)

    def __str__(self) -> str:
        return self.__repr__()


class RemoteExceptionWrapper:
    def __init__(self, e_type: type, e_value: BaseException, traceback: Optional[TracebackType]) -> None:

        self.e_type = e_type
        self.e_value = e_value
        self.e_traceback = None if traceback is None else Traceback(traceback)
        if e_value.__cause__ is None:
            self.cause = None
        else:
            cause = e_value.__cause__
            self.cause = self.__class__(type(cause), cause, cause.__traceback__)

    def reraise(self) -> None:

        t = self.e_type

        # the type is logged here before deserialising v and tb
        # because occasionally there are problems deserialising the
        # value (see #785, #548) and the fix is related to the
        # specific exception type.
        logger.debug("Reraising exception of type {}".format(self.e_type))

        v = self.get_exception()

        reraise(t, v, v.__traceback__)

    def get_exception(self) -> BaseException:
        v = self.e_value
        if self.cause is not None:
            v.__cause__ = self.cause.get_exception()
        if self.e_traceback is not None:
            tb = self.e_traceback.as_traceback()
            return v.with_traceback(tb)
        else:
            return v


R = TypeVar('R')

# There appears to be no solution to typing this without a mypy plugin.
# The reason is because wrap_error maps a Callable[[X...], R] to a Callable[[X...], Union[R, R2]].
# However, there is no provision in Python typing for pattern matching all possible types of
# callable arguments. This is because Callable[] is, in the infinite wisdom of the typing module,
# only used for callbacks: "There is no syntax to indicate optional or keyword arguments; such
# function types are rarely used as callback types.".
# The alternative supported by the typing module, of saying Callable[..., R] ->
#   Callable[..., Union[R, R2]] results in no pattern matching between the first and second
# ellipsis.
# Yet another bogus solution that was here previously would simply define wrap_error as
#   wrap_error(T) -> T, where T was a custom TypeVar. This obviously missed the fact that
# the returned function had its return signature modified.
# Ultimately, the best choice appears to be Callable[..., R] -> Callable[..., Union[R, ?Exception]],
#  since it results in the correct type specification for the return value(s) while treating the
#  arguments as Any.


def wrap_error(func: Callable[..., R]) -> Callable[..., Union[R, RemoteExceptionWrapper]]:
    @wraps(func)
    def wrapper(*args: object, **kwargs: object) -> Any:
        import sys
        from parsl.app.errors import RemoteExceptionWrapper
        try:
            return func(*args, **kwargs)
        except Exception:
            return RemoteExceptionWrapper(*sys.exc_info())
    return wrapper
