# -*- coding: utf-8 -*-
# @Time    : 2020/12/23 01:58:09 
# @Author  : ddvv
# @Site    : https://ddvvmmzz.github.io
# @File    : plugins.py 
# @Software: Visual Studio Code


import os
import importlib
import logging
import mmpi

from mmpi.common.filetypes import get_support_file_type
from mmpi.common.exceptions import DependencyError, ProcessingError

log = logging.getLogger(__name__)


def enumerate_plugins(dirpath, module_prefix, namespace, class_,
                      attributes={}, as_dict=False):
    """Import plugins of type `class` located at `dirpath` into the
    `namespace` that starts with `module_prefix`. If `dirpath` represents a
    filepath then it is converted into its containing directory. The
    `attributes` dictionary allows one to set extra fields for all imported
    plugins. Using `as_dict` a dictionary based on the module name is
    returned."""
    if os.path.isfile(dirpath):
        dirpath = os.path.dirname(dirpath)

    for fname in os.listdir(dirpath):
        if fname.endswith(".py") and not fname.startswith("__init__"):
            module_name, _ = os.path.splitext(fname)
            try:
                importlib.import_module(
                    "%s.%s" % (module_prefix, module_name)
                )
            except ImportError as e:
                raise ImportError(
                    "Unable to load theplugin at %s: %s. Please "
                    "review its contents and/or validity!" % (fname, e)
                )

    subclasses = class_.__subclasses__()[:]

    plugins = []
    while subclasses:
        subclass = subclasses.pop(0)

        # Include subclasses of this subclass (there are some subclasses, e.g.,
        # LibVirtMachinery, that fail the fail the following module namespace
        # check and as such we perform this logic here).
        subclasses.extend(subclass.__subclasses__())

        # Check whether this subclass belongs to the module namespace that
        # we're currently importing. It should be noted that parent and child
        # namespaces should fail the following if-statement.
        if module_prefix != ".".join(subclass.__module__.split(".")[:-1]):
            continue

        namespace[subclass.__name__] = subclass
        for key, value in attributes.items():
            setattr(subclass, key, value)

        plugins.append(subclass)

    if as_dict:
        ret = {}
        for plugin in plugins:
            ret[plugin.__module__.split(".")[-1]] = plugin
        return ret

    return sorted(plugins, key=lambda x: x.__name__.lower())


class RunProcessing(object):
    """Analysis Results Processing Engine.

    This class handles the loading and execution of the processing modules.
    It executes the enabled ones sequentially and generates a dictionary which
    is then passed over the reporting engine.
    """

    def __init__(self, data):
        """@param task: task dictionary of the analysis to process."""
        self.file_type = get_support_file_type(data.get('file_type'))
        self.file_content = data.get('content')

    def process(self, module, results):
        """Run a processing module.
        @param module: processing module to run.
        @param results: results dict.
        @return: results generated by module.
        """
        if not module.enabled:
            return None, None

        # Initialize the specified processing module.
        try:
            current = module()
        except:
            log.warning(
                "Failed to load the processing module: %s",
                module, extra={"file_content": self.file_content}
            )
            return None, None

        # Give it the path to the analysis results.
        current.set_content(self.file_content)
        current.set_type(self.file_type)
        current.set_results(results)

        try:
            # Run the processing module and retrieve the generated data to be
            # appended to the general results container.
            data = current.run()

            log.debug(
                "Executed processing module \"%s\" for task #%s",
                current.__class__.__name__, self.file_content
            )

            # If succeeded, return they module's key name and the data.
            return current.key, data
        except DependencyError as e:
            log.warning(
                "The processing module \"%s\" has missing dependencies: %s",
                current.__class__.__name__, e
            )
        except ProcessingError as e:
            log.warning(
                "The processing module \"%s\" returned the following "
                "error: %s",
                current.__class__.__name__, e
            )
        except Exception as e:
            log.warning(
                "Failed to run the processing module \"%s\" for task #%s:",
                current.__class__.__name__, self.file_content
            )

        return None, None

    def run(self):
        """Run all processing modules and all signatures.
        @return: processing results.
        """
        # This is the results container. It's what will be used by all the
        # reporting modules to make it consumable by humans and machines.
        # It will contain all the results generated by every processing
        # module available. Its structure can be observed through the JSON
        # dump in the analysis' reports folder. (If jsondump is enabled.)
        # We friendly call this "fat dict".
        results = {
            "_temp": {},
        }

        # Uses plain machine configuration as input.

        # Order modules using the user-defined sequence number.
        # If none is specified for the modules, they are selected in
        # alphabetical order.
        processing_list = mmpi.processing.plugins

        # If no modules are loaded, return an empty dictionary.
        if processing_list:
            processing_list.sort(key=lambda module: module.order)

            # Run every loaded processing module.
            for module in processing_list:
                key, result = self.process(module, results)

                # If the module provided results, append it to the fat dict.
                if key and result:
                    results['type'] = key
                    results['value'] = result
        else:
            log.info("No processing modules loaded")

        results.pop("_temp", None)

        # Return the fat dict.
        return results


class RunSignatures(object):
    """Run Signatures."""
    available_signatures = []

    def __init__(self, results):
        self.results = results
        self.matched = []

        # Initialize each applicable Signature.
        self.signatures = []
        for signature in self.available_signatures:
            self.signatures.append(signature(self))

    @classmethod
    def init_once(cls):
        cls.available_signatures = []

        # Gather all enabled & up-to-date Signatures.
        for signature in mmpi.signatures:
            if cls.should_load_signature(signature):
                cls.available_signatures.append(signature)

        # Sort Signatures by their order.
        cls.available_signatures.sort(key=lambda sig: sig.order)

    @classmethod
    def should_load_signature(cls, signature):
        """Should the given signature be enabled for this analysis?"""
        if not signature.enabled or signature.name is None:
            return False

        if not cls.check_signature_version(signature):
            return False

        if hasattr(signature, "enable") and callable(signature.enable):
            if not signature.enable():
                return False

        return True

    @classmethod
    def check_signature_version(cls, sig):
        """Check signature version.
        @param sig: signature class/instance to check.
        @return: check result.
        """
        if hasattr(sig, "run"):
            log.warning(
                "This signatures features one or more deprecated functions "
                "which indicates that it is very likely an old-style "
                "signature. Please upgrade this signature: %s.", sig.name
            )
            return False

        return True

    def call_signature(self, signature, handler, *args, **kwargs):
        """Wrapper to call into 3rd party signatures. This wrapper yields the
        event to the signature and handles matched signatures recursively."""
        try:
            if not signature.matched and handler(*args, **kwargs):
                signature.matched = True
                for sig in self.signatures:
                    self.call_signature(sig, sig.on_signature, signature)
        except NotImplementedError:
            return False
        except:
            log.warning(
                "Failed to run '%s' of the %s signature",
                handler.__name__, signature.name
            )
        return True

    def run(self):
        """Run signatures."""
        # Allow signatures to initialize themselves.
        for signature in self.signatures:
            signature.init()

        log.debug("Running %d signatures", len(self.signatures))

        # Yield completion events to each signature.
        for sig in self.signatures:
            self.call_signature(sig, sig.on_complete)

        score, configs = 0, []
        for signature in self.signatures:
            if not signature.matched:
                continue

            log.debug(
                "matched signature: %s", signature.name, extra={
                    "action": "signature.match", "status": "success",
                    "signature": signature.name,
                    "severity": signature.severity,
                }
            )
            self.matched.append(signature.results())
            score += signature.severity

        # Sort the matched signatures by their severity level and put them
        # into the results dictionary.
        self.matched.sort(key=lambda key: key["severity"])
        self.results.append({"type": "signatures", "value": {
                            "infos": self.matched, "datas": list()}})
