from functools import wraps
from copy import deepcopy
from traitlets import TraitError
from traitlets.config.loader import (
    Config,
)
from jupyter_core.application import JupyterApp
from jupyter_server.serverapp import ServerApp
from jupyter_server.extension.application import ExtensionApp
from .traits import NotebookAppTraits


def NBAPP_AND_SVAPP_SHIM_MSG(trait_name): return (
    "'{trait_name}' was found in both NotebookApp "
    "and ServerApp. This is likely a recent change. "
    "This config will only be set in NotebookApp. "
    "Please check if you should also config these traits in "
    "ServerApp for your purpose.".format(
        trait_name=trait_name,
    )
)


def NBAPP_TO_SVAPP_SHIM_MSG(trait_name): return (
    "'{trait_name}' has moved from NotebookApp to "
    "ServerApp. This config will be passed to ServerApp. "
    "Be sure to update your config before "
    "our next release.".format(
        trait_name=trait_name,
    )
)


def EXTAPP_AND_NBAPP_AND_SVAPP_SHIM_MSG(trait_name, extapp_name): return (
    "'{trait_name}' is found in {extapp_name}, NotebookApp, "
    "and ServerApp. This is a recent change. "
    "This config will only be set in {extapp_name}. "
    "Please check if you should also config these traits in "
    "NotebookApp and ServerApp for your purpose.".format(
        trait_name=trait_name,
        extapp_name=extapp_name
    )
)


def EXTAPP_AND_SVAPP_SHIM_MSG(trait_name, extapp_name): return (
    "'{trait_name}' is found in both {extapp_name} "
    "and ServerApp. This is a recent change. "
    "This config will only be set in {extapp_name}. "
    "Please check if you should also config these traits in "
    "ServerApp for your purpose.".format(
        trait_name=trait_name,
        extapp_name=extapp_name
    )
)


def EXTAPP_AND_NBAPP_SHIM_MSG(trait_name, extapp_name): return (
    "'{trait_name}' is found in both {extapp_name} "
    "and NotebookApp. This is a recent change. "
    "This config will only be set in {extapp_name}. "
    "Please check if you should also config these traits in "
    "NotebookApp for your purpose.".format(
        trait_name=trait_name,
        extapp_name=extapp_name
    )
)


def NOT_EXTAPP_NBAPP_AND_SVAPP_SHIM_MSG(trait_name, extapp_name): return (
    "'{trait_name}' is not found in {extapp_name}, but "
    "it was found in both NotebookApp "
    "and ServerApp. This is likely a recent change. "
    "This config will only be set in ServerApp. "
    "Please check if you should also config these traits in "
    "NotebookApp for your purpose.".format(
        trait_name=trait_name,
        extapp_name=extapp_name
    )
)


def EXTAPP_TO_SVAPP_SHIM_MSG(trait_name, extapp_name): return (
    "'{trait_name}' has moved from {extapp_name} to "
    "ServerApp. Be sure to update your config before "
    "our next release.".format(
        trait_name=trait_name,
        extapp_name=extapp_name
    )
)


def EXTAPP_TO_NBAPP_SHIM_MSG(trait_name, extapp_name): return (
    "'{trait_name}' has moved from {extapp_name} to "
    "NotebookApp. Be sure to update your config before "
    "our next release.".format(
        trait_name=trait_name,
        extapp_name=extapp_name
    )
)


# A tuple of traits that shouldn't be shimmed or throw any
# warnings of any kind.
IGNORED_TRAITS = ("open_browser", "log_level", "log_format", "default_url")


class NotebookConfigShimMixin:
    """A Mixin class for shimming configuration from
    NotebookApp to ServerApp. This class handles warnings, errors,
    etc.

    This class should be used during a transition period for apps
    that are switching from depending on NotebookApp to ServerApp.

    After one release cycle, this class can be safely removed
    from the inheriting class.

    TL;DR

    The entry point to shimming is at the `update_config` method.
    Once traits are loaded, before updating config across all
    configurable objects, this class injects a method to reroute
    traits to their *most logical* classes.

    This class raises warnings when:
        1. a trait has moved.
        2. a trait is redundant across classes.

    Redundant traits across multiple classes now must be
    configured separately, *or* removed from their old
    location to avoid this warning.

    For a longer description on how individual traits are handled,
    read the docstring under `shim_config_from_notebook_to_jupyter_server`.
    """

    @wraps(JupyterApp.update_config)
    def update_config(self, config):
        # Shim traits to handle transition from NotebookApp to ServerApp
        shimmed_config = self.shim_config_from_notebook_to_jupyter_server(
            config)
        super().update_config(shimmed_config)

    def shim_config_from_notebook_to_jupyter_server(self, config):
        """Reorganizes a config object to reroute traits to their expected destinations
        after the transition from NotebookApp to ServerApp.

        A detailed explanation of how traits are handled:

        1. If the argument is prefixed with `ServerApp`,
            pass this trait to `ServerApp`.
        2. If the argument is prefixed with `NotebookApp`,
            * If the argument is a trait of `NotebookApp` *and* `ServerApp`:
                1. Raise a warning—**for the extension developers**—that
                    there's redundant traits.
                2. Pass trait to `NotebookApp`.
            * If the argument is a trait of just `ServerApp` only
                (i.e. the trait moved from `NotebookApp` to `ServerApp`):
                1. Raise a "this trait has moved" **for the user**.
                3. Pass trait to `ServerApp`.
            * If the argument is a trait of `NotebookApp` only, pass trait
                to `NotebookApp`.
            * If the argument is not found in any object, raise a
                `"Trait not found."` error.
        3. If the argument is prefixed with `ExtensionApp`:
            * If the argument is a trait of `ExtensionApp`,
                `NotebookApp`, and `ServerApp`,
                1. Raise a warning about redundancy.
                2. Pass to the ExtensionApp
            * If the argument is a trait of `ExtensionApp` and `NotebookApp`,
                1. Raise a warning about redundancy.
                2. Pass to ExtensionApp.
            * If the argument is a trait of `ExtensionApp` and `ServerApp`,
                1. Raise a warning about redundancy.
                2. Pass to ExtensionApp.
            * If the argument is a trait of `ExtensionApp`.
                1. Pass to ExtensionApp.
            * If the argument is a trait of `NotebookApp` but not `ExtensionApp`,
                1. Raise a warning that trait has likely moved to NotebookApp.
                2. Pass to NotebookApp
            * If the arguent is a trait of `ServerApp` but not `ExtensionApp`,
                1. Raise a warning that the trait has likely moved to ServerApp.
                2. Pass to ServerApp.
            * else
                * Raise a TraitError: "trait not found."
        """
        extapp_name = self.__class__.__name__

        # Pop out the various configurable objects that we need to evaluate.
        nbapp_config = config.pop('NotebookApp', {})
        svapp_config = config.pop('ServerApp', {})
        extapp_config = config.pop(extapp_name, {})

        # Created shimmed configs.
        # Leave the rest of the config alone.
        config_shim = deepcopy(config)
        svapp_config_shim = {}
        nbapp_config_shim = {}
        extapp_config_shim = {}

        extapp_traits = (
            self.__class__.class_trait_names() +
            ExtensionApp.class_trait_names()
        )
        svapp_traits = ServerApp.class_trait_names()
        nbapp_traits = (
            NotebookAppTraits.class_trait_names() +
            ExtensionApp.class_trait_names()
        )

        # 1. Handle ServerApp traits.
        svapp_config_shim.update(svapp_config)

        # 2. Handle NotebookApp traits.
        warning_msg = None
        for trait_name, trait_value in nbapp_config.items():
            in_svapp = trait_name in svapp_traits
            in_nbapp = trait_name in nbapp_traits
            if trait_name in IGNORED_TRAITS:
                # Pass trait through without any warning message.
                nbapp_config_shim.update({trait_name: trait_value})
            elif in_svapp and in_nbapp:
                warning_msg = NBAPP_AND_SVAPP_SHIM_MSG(trait_name)
                nbapp_config_shim.update({trait_name: trait_value})
            elif in_svapp:
                warning_msg = NBAPP_TO_SVAPP_SHIM_MSG(trait_name)
                svapp_config_shim.update({trait_name: trait_value})
            elif in_nbapp:
                nbapp_config_shim.update({trait_name: trait_value})
            else:
                raise TraitError("Trait, {}, not found.".format(trait_name))

            # Raise a warning if it's given.
            if warning_msg:
                self.log.warning(warning_msg)

        # 3. Handle ExtensionApp traits.
        warning_msg = None
        for trait_name, trait_value in extapp_config.items():
            in_extapp = trait_name in extapp_traits
            in_svapp = trait_name in svapp_traits
            in_nbapp = trait_name in nbapp_traits
            if trait_name in IGNORED_TRAITS:
                # Pass trait through without any warning message.
                extapp_config_shim.update({trait_name: trait_value})
            elif all([in_extapp, in_svapp, in_nbapp]):
                warning_msg = EXTAPP_AND_NBAPP_AND_SVAPP_SHIM_MSG(
                    trait_name,
                    extapp_name
                )
                extapp_config_shim.update({trait_name: trait_value})
            elif in_extapp and in_svapp:
                warning_msg = EXTAPP_AND_SVAPP_SHIM_MSG(
                    trait_name,
                    extapp_name
                )
                extapp_config_shim.update({trait_name: trait_value})
            elif in_extapp and in_nbapp:
                warning_msg = EXTAPP_AND_NBAPP_SHIM_MSG(
                    trait_name,
                    extapp_name
                )
                extapp_config_shim.update({trait_name: trait_value})
            elif in_extapp:
                extapp_config_shim.update({trait_name: trait_value})
            elif in_svapp and in_nbapp:
                warning_msg = NOT_EXTAPP_NBAPP_AND_SVAPP_SHIM_MSG(
                    trait_name,
                    extapp_name
                )
                svapp_config_shim.update({trait_name: trait_value})
            elif in_svapp:
                warning_msg = EXTAPP_TO_SVAPP_SHIM_MSG(
                    trait_name,
                    extapp_name
                )
                svapp_config_shim.update({trait_name: trait_value})
            elif in_nbapp:
                warning_msg = EXTAPP_TO_NBAPP_SHIM_MSG(
                    trait_name,
                    extapp_name
                )
                nbapp_config_shim.update({trait_name: trait_value})
            else:
                raise TraitError("Trait, {}, not found.".format(trait_name))

            # Raise warning if one is given
            if warning_msg:
                self.log.warning(warning_msg)

        # Build config for shimmed traits.
        new_config = Config({
            'NotebookApp': nbapp_config_shim,
            'ServerApp': svapp_config_shim,
        })
        if extapp_config_shim:
            new_config.update(Config({
                self.__class__.__name__: extapp_config_shim
            }))
        # Update the full config with new values
        config_shim.update(new_config)
        return config_shim
