import yaml
import importlib

from schema import Schema, Or, SchemaError
from .context import Context

FORMAT_MPD = "mpd"
FORMAT_M3U8_MAIN = "m3u8_main"
FORMAT_M3U8_MEDIA = "m3u8_media"

_supported_formats = [FORMAT_MPD, FORMAT_M3U8_MAIN, FORMAT_M3U8_MEDIA]


class Config:

    _schema = Schema(
        {
            Or(*_supported_formats): [
                {
                    # plugins path: configuration or empty in case you
                    # want to receive it from a previous plugin
                    str: Or(list, dict, None)
                }
            ]
        }
    )

    @staticmethod
    def validate(config):
        return Config._schema.validate(config)


class Pipeline:

    # TODO needs to be shared and thus elsewhere.
    SUPPORTED_MANIFEST_FORMATS = _supported_formats

    @staticmethod
    def build_pipeline(yaml_file):
        with open(yaml_file, "rb") as stream:
            buffer = stream.read()

        return Pipeline.build_pipeline_from_string(buffer)

    @staticmethod
    def build_pipeline_from_string(buffer):
        """Static method to build a pipeline from a yaml configuration file."""
        try:
            # Trick: using Buffers to improve error reporting
            #  by pyyaml! Buffers are better handled than streams.
            pipeline_def_raw = yaml.safe_load(buffer)
        except yaml.YAMLError as e:
            Context.log_error(
                f"Invalid yaml configuration in {buffer}:" "  {e}\n"
            )
            raise e

        try:
            # will raise exception if overall yml file config is invalid
            pipeline_def = Config.validate(pipeline_def_raw)
        except SchemaError as e:
            Context.log_error(
                f"Invalid Yaml configuration:\n    {e.autos[-1]}"
            )
            raise e

        # A config file can contain a pipeline definition per manifest
        # format

        manifest_format, plugin_def = next(iter(pipeline_def.items()))
        plugins_list = []

        for p in plugin_def:
            plugin_name, plugin_config = next(iter(p.items()))
            # Here you need to create the plugin
            try:
                # Remember that python imports just once, no matter
                # how many times you call this
                Context.log_debug(f"Attempting to import {plugin_name}")
                plugin = importlib.import_module(plugin_name)
                Context.log_debug(f"Imported {plugin}")
                importlib.invalidate_caches()
            except ImportError as e:
                Context.log_error(
                    f"Unable to find python plugin defined in {buffer}\n"
                    f" Exception {e}"
                )
                raise e
            plugin_class_instance = getattr(plugin, "Plugin")()
            # Base class plugin method to specialize processing
            #  function on the specific format
            plugin_class_instance.onCreate(manifest_format, plugin_config)
            try:
                if plugin_config is None:
                    # This is a plugin that will receive its config
                    #  from another plugin and not from the yml file.
                    plugins_list.append(plugin_class_instance)
                elif plugin_class_instance.validate_config():
                    # This is one that gets its config from a yml file
                    # so we had to know if that is correct
                    plugins_list.append(plugin_class_instance)
                else:
                    raise SchemaError(
                        f'The provided config {plugin_config} is not valid!'
                    )
            except SchemaError as e:
                errors = set(
                    [
                        err.rsplit("validate")[-1]
                        for err in e.autos
                        if err is not None and "validate" in err
                    ]
                )
                error_messages = ""
                for err in errors:
                    error_messages += err + "\n"
                Context.log_error(
                    f"Invalid configuration for plugin {plugin.__name__}:\n    {error_messages}"
                )
                raise e

        return Pipeline(
            manifest_format,
            plugins_list,
            yaml.dump(pipeline_def, default_flow_style=False, allow_unicode=True),
        )

    def __init__(self, manifest_format, plugins_list, yaml_configuration):
        self.format = manifest_format
        self._pipeline = plugins_list
        self.yaml_configuration = yaml_configuration

    def run(self, input_manifest=None, storage={}, **kwargs):
        # Process across the pipeline
        # The storage dictionary can be used by plugins to propagate anything
        #  down the pipeline to subsequent plugins
        for plugin in self._pipeline:
            plugin.process(manifest=input_manifest, storage=storage)
