from __future__ import absolute_import, print_function, unicode_literals

import functools

import six

from compat_patcher_core.exceptions import SkipFixerException


class PatchingRunner(object):
    """
    This class is in charge of fetching relevant fixers from the registry,
    and applying them in a proper order, while skipping those who have already been
    applied (i.e same family name and ID) and those which raised SkipFixerException.
    """

    # Helpful for autodocumentation
    settings_keys_used = [
        "include_fixer_ids",
        "include_fixer_families",
        "exclude_fixer_ids",
        "exclude_fixer_families",
    ]

    _all_applied_fixers = []  # Class attribute with qualified fixer names!

    def __init__(self, settings, patching_registry, patching_utilities):
        assert settings, settings
        self._settings = settings
        self._patching_registry = patching_registry
        self._patching_utilities = patching_utilities

    @classmethod
    def _clear_all_applied_fixers(cls):  # For testing only!
        del cls._all_applied_fixers[:]

    def _get_patcher_setting(self, name):
        """
        Returns the value of a patcher setting.
        """
        assert name in self.settings_keys_used  # To track coherence

        value = self._settings[name]

        # For now, patching utilities validate their own settings, so we just check filters
        if name.startswith("include") or name.startswith("exclude"):
            assert value in ("*", None) or (
                isinstance(value, (list, tuple))
                and all(isinstance(f, six.string_types) for f in value)
            ), value

        return value

    def _apply_selected_fixers(self, fixers):
        fixers_just_applied = []
        for fixer in fixers:

            fixer_qualified_name = fixer["fixer_qualified_name"]

            if fixer_qualified_name not in self._all_applied_fixers:
                self._patching_utilities.emit_log(
                    "Compat fixer {}->{} is getting applied".format(
                        fixer["fixer_family"], fixer["fixer_id"]
                    ),
                    level="INFO",
                )
                try:
                    fixer["fixer_callable"](self._patching_utilities)
                    self._all_applied_fixers.append(fixer_qualified_name)
                    fixers_just_applied.append(fixer["fixer_id"])
                except SkipFixerException as e:
                    self._patching_utilities.emit_log(
                        "Compat fixer {}->{} was actually not applied, reason: {}".format(
                            fixer["fixer_family"], fixer["fixer_id"], e
                        ),
                        level="WARNING",
                    )
            else:
                self._patching_utilities.emit_log(
                    "Compat fixer {}->{} was already applied".format(["fixer_family"], fixer["fixer_id"]),
                    level="WARNING",
                )
        return fixers_just_applied

    def _get_sorted_relevant_fixers(self):

        # For now, we don't need to be able to force-send a `current_software_version`
        fixers_settings = dict(
            include_fixer_ids=self._get_patcher_setting("include_fixer_ids"),
            include_fixer_families=self._get_patcher_setting("include_fixer_families"),
            exclude_fixer_ids=self._get_patcher_setting("exclude_fixer_ids"),
            exclude_fixer_families=self._get_patcher_setting("exclude_fixer_families"),
        )
        log = functools.partial(self._patching_utilities.emit_log, level="DEBUG")
        relevant_fixers = self._patching_registry.get_relevant_fixers(
            log=log, **fixers_settings
        )

        # REVERSED order is necessary for backwards compatibility, more advanced
        # sorting might be introduced to help forward-compatibility fixers...
        relevant_fixers.sort(
            key=lambda x: (x["fixer_reference_version"], x["fixer_id"]), reverse=True
        )

        return relevant_fixers

    def patch_software(self):
        """Patch the software according to plans.

        Return a dict with, at least field "fixers_just_applied", the list of fixers that
        were successfully applied during this call.
        """
        relevant_fixers = self._get_sorted_relevant_fixers()
        fixers_just_applied = self._apply_selected_fixers(relevant_fixers)
        return dict(fixers_just_applied=fixers_just_applied)
