"""This module defines a few custom Exception classes, and it provides the means for Exceptions to
be added to the Heartbeat result files of Experiments

Related
-------
:mod:`hyperparameter_hunter.reporting`
    This module executes :func:`hyperparameter_hunter.exception_handler.hook_exception_handler` to
    ensure that any raised Exceptions are also recorded in the Heartbeat files of the Experiment for
    which the Exception was raised in order to assist in debugging"""
##################################################
# Import Miscellaneous Assets
##################################################
import logging
import sys

logger = logging.getLogger(__name__)

# noinspection PyProtectedMember
stream_handler = logging._StderrHandler()
# stream_handler = logging.StreamHandler(stream=sys.stdout)
logger.addHandler(stream_handler)


def handle_exception(exception_type, exception_value, exception_traceback):
    """Intercept raised exceptions to ensure they are included in an Experiment's log files

    Parameters
    ----------
    exception_type: Exception
        The class type of the exception that was raised
    exception_value: Str
        The message produced by the exception
    exception_traceback: Exception.traceback
        The traceback provided by the raised exception

    Raises
    ------
    SystemExit
        If `exception_type` is a subclass of KeyboardInterrupt"""
    if issubclass(exception_type, KeyboardInterrupt):
        logging.error("KEYBOARD INTERRUPT!")
        sys.__excepthook__(exception_type, exception_value, exception_traceback)
        raise SystemExit

    logging.critical(
        "Uncaught exception!   {}: {}".format(exception_type.__name__, exception_value),
        exc_info=(exception_type, exception_value, exception_traceback),
    )


def hook_exception_handler():
    """Set `sys.excepthook` to :func:`hyperparameter_hunter.exception_handler.handle_exception`"""
    sys.excepthook = handle_exception


##################################################
# Custom Exceptions
##################################################
class EnvironmentInactiveError(Exception):
    def __init__(self, message=None, extra=""):
        """Exception raised when an active instance of
        :class:`hyperparameter_hunter.environments.Environment` is not detected

        Parameters
        ----------
        message: String, or None, default=None
            A message to provide upon raising `EnvironmentExceptionError`
        extra: String, default=''
            Extra content to append onto the end of `message` before raising the Exception"""
        if not message:
            message = "You must activate a valid instance of :class:`environment.Environment`"
        super(EnvironmentInactiveError, self).__init__(message + extra)


class EnvironmentInvalidError(Exception):
    def __init__(self, message=None, extra=""):
        """Exception raised when there is an active instance of
        :class:`hyperparameter_hunter.environments.Environment`, but it is invalid for some reason

        Parameters
        ----------
        message: String, or None, default=None
            A message to provide upon raising `EnvironmentInvalidError`
        extra: String, default=''
            Extra content to append onto the end of `message` before raising the Exception"""
        if not message:
            message = (
                "The currently active Environment is invalid. Please review proper "
                "Environment instantiation"
            )
        super(EnvironmentInvalidError, self).__init__(message + extra)


class RepeatedExperimentError(Exception):
    def __init__(self, message=None, extra=""):
        """Exception raised when a saved Experiment is found with the same hyperparameters as the
        Experiment being executed

        Parameters
        ----------
        message: String, or None, default=None
            A message to provide upon raising `RepeatedExperimentError`
        extra: String, default=''
            Extra content to append onto the end of `message` before raising the Exception"""
        if not message:
            message = (
                "An Experiment with identical hyperparameters has already been conducted "
                "and has saved results"
            )
        super(RepeatedExperimentError, self).__init__(message + extra)


class IncompatibleCandidateError(Exception):
    def __init__(self, candidate, template):
        """Exception raised when a `candidate` hyperparameter set is incompatible with a `template`

        Parameters
        ----------
        candidate: Any
            Hyperparameter set that is incompatible with the choices/concrete values of `template`
        template: Any
            Hyperparameter set defined by
            :meth:`~hyperparameter_hunter.optimization.protocol_core.BaseOptPro.forge_experiment`.
            May include any combination of space choices and concrete values"""
        message = (
            "The `candidate` hyperparameters are incompatible with the `template`\n"
            f"   - `candidate`: {candidate}\n"
            f"   - `template`:  {template}"
        )
        super(IncompatibleCandidateError, self).__init__(message)


class ContinueRemap(Exception):
    def __str__(self):
        return "Just keep doing what you were doing"


##################################################
# Deprecation Warnings
##################################################
class DeprecatedWarning(DeprecationWarning):
    """Warning class for deprecated callables. This is a specialization of the built-in
    :class:`DeprecationWarning`, adding parameters that allow us to get information into the __str__
    that ends up being sent through the :mod:`warnings` system. The attributes aren't able to be
    retrieved after the warning gets raised and passed through the system as only the class--not the
    instance--and message are what gets preserved

    Parameters
    ----------
    obj_name: String
        The name of the callable being deprecated
    v_deprecate: String
        The version that `obj` is deprecated in
    v_remove: String
        The version that `obj` gets removed in
    details: String, default=""
        Deprecation details, such as directions on what to use instead of the deprecated code"""

    def __init__(self, obj_name, v_deprecate, v_remove, details=""):
        # Docstring only works under class, not __init__. Likely due to being an exception class
        self.obj_name = obj_name
        self.v_deprecate = v_deprecate
        self.v_remove = v_remove
        self.details = details
        super(DeprecatedWarning, self).__init__(obj_name, v_deprecate, v_remove, details)

    def __str__(self):
        parts = dict(
            obj_name=self.obj_name,
            deprecated=" as of {}".format(self.v_deprecate) if self.v_deprecate else "",
            removed=" and will be removed in {}".format(self.v_remove) if self.v_remove else "",
            period="." if any([self.v_deprecate, self.v_remove, self.details]) else "",
            details=" {}".format(self.details) if self.details else "",
        )

        return "{obj_name} is deprecated{deprecated}{removed}{period}{details}".format(**parts)


class UnsupportedWarning(DeprecatedWarning):
    """Warning class for callable to warn that it is being unsupported"""

    def __str__(self):
        return "{} is unsupported as of {}. {}".format(self.obj_name, self.v_remove, self.details)


if __name__ == "__main__":
    pass
