import inspect
import logging
from importlib import import_module
from pkgutil import iter_modules
from types import ModuleType
from typing import Any, Optional, Protocol, Type, runtime_checkable, cast

from flamapy.core.config import PLUGIN_PATHS
from flamapy.core.exceptions import OperationNotFound
from flamapy.core.exceptions import TransformationNotFound
from flamapy.core.exceptions import ConfigurationNotFound
from flamapy.core.models import VariabilityModel
from flamapy.core.operations import Operation
from flamapy.core.plugins import Operations, Plugin, Plugins
from flamapy.core.transformations import Transformation
from flamapy.core.transformations.text_to_model import TextToModel
from flamapy.core.transformations.model_to_model import ModelToModel
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration


LOGGER = logging.getLogger("discover")


@runtime_checkable
class OperationWithConfiguration(Protocol):
    def set_configuration(self, configuration: Configuration) -> None:
        pass


def filter_modules_from_plugin_paths() -> list[ModuleType]:
    results: list[ModuleType] = []
    for path in PLUGIN_PATHS:
        try:
            module: ModuleType = import_module(path)
            results.append(module)
        except ModuleNotFoundError:
            LOGGER.exception("ModuleNotFoundError %s", path)
    return results


class DiscoverMetamodels:
    def __init__(self) -> None:
        self.module_paths = filter_modules_from_plugin_paths()
        self.plugins: Plugins = self.discover()

    def search_classes(self, module: ModuleType) -> list[Any]:
        classes = []
        for _, file_name, ispkg in iter_modules(module.__path__, module.__name__ + "."):
            if ispkg:
                classes += self.search_classes(import_module(file_name))
            else:
                _file = import_module(file_name)
                classes += [
                    member
                    for member in inspect.getmembers(_file, inspect.isclass)
                    if member[1].__module__ == _file.__name__
                ]

        return classes

    def discover(self) -> Plugins:
        plugins = Plugins()
        for pkg in self.module_paths:
            for _, plugin_name, ispkg in iter_modules(pkg.__path__, pkg.__name__ + "."):
                if not ispkg:
                    continue
                module = import_module(plugin_name)
                plugin = Plugin(module=module)

                classes = self.search_classes(module)

                for _, _class in classes:
                    if not _class.__module__.startswith(module.__package__):
                        continue  # Exclude modules not in current package
                    inherit = _class.mro()

                    if Operation in inherit:
                        plugin.append_operation(_class)
                    elif Transformation in inherit:
                        plugin.append_transformations(_class)
                    elif VariabilityModel in inherit:
                        plugin.variability_model = _class
                plugins.append(plugin)
        return plugins

    def reload(self) -> None:
        self.plugins = self.discover()

    def get_operations(self) -> list[Type[Operation]]:
        """Get the operations for all modules"""
        operations: list[Type[Operation]] = []
        for plugin in self.plugins:
            operations += plugin.operations
        return operations

    def get_name_operations(self) -> list[str]:
        operations = []
        for operation in self.get_operations():
            operations.append(operation.__name__)
            base = operation.__base__.__name__ if operation.__base__ else ""
            if base != "ABC":
                operations.append(base)

        return operations

    def get_transformations(self) -> list[Type[Transformation]]:
        """Get transformations for all modules"""
        transformations: list[Type[Transformation]] = []
        for plugin in self.plugins:
            transformations += plugin.transformations
        return transformations

    def get_transformations_t2m(self) -> list[Type[TextToModel]]:
        """Get t2m transformations for all modules"""

        transformations: list[Type[TextToModel]] = []
        for plugin in self.plugins:
            transformations += [t for t in plugin.transformations if issubclass(t, TextToModel)]
        return transformations

    def get_transformations_m2m(self) -> list[Type[ModelToModel]]:
        """Get m2m transformations for all modules"""

        transformations: list[Type[ModelToModel]] = []
        for plugin in self.plugins:
            transformations += [t for t in plugin.transformations if issubclass(t, ModelToModel)]
        return transformations

    def get_operations_by_plugin(self, plugin_name: str) -> Operations:
        return self.plugins.get_operations_by_plugin_name(plugin_name)

    def get_plugins_with_operation(self, operation_name: str) -> list[Plugin]:
        return [
            plugin
            for plugin in self.plugins
            if operation_name in self.get_name_operations_by_plugin(plugin.name)
        ]

    def get_name_operations_by_plugin(self, plugin_name: str) -> list[str]:
        operations = []
        for operation in self.get_operations_by_plugin(plugin_name):
            operations.append(operation.__name__)
            base = operation.__base__.__name__ if operation.__base__ else ""
            if base != "ABC":
                operations.append(base)

        return operations

    def get_variability_models(self) -> list[VariabilityModel]:
        return self.plugins.get_variability_models()

    def get_plugins(self) -> list[str]:
        return self.plugins.get_plugin_names()

    def use_transformation_m2t(self, src: VariabilityModel, dst: str) -> str:
        plugin = self.plugins.get_plugin_by_variability_model(src)
        return plugin.use_transformation_m2t(src, dst)

    def use_transformation_t2m(self, src: str, dst: str) -> VariabilityModel:
        plugin = self.plugins.get_plugin_by_extension(dst)
        return plugin.use_transformation_t2m(src)

    def use_transformation_m2m(self, src: VariabilityModel, dst: str) -> VariabilityModel:
        plugin = self.plugins.get_plugin_by_extension(dst)
        return plugin.use_transformation_m2m(src, dst)

    def get_operation(self, src: VariabilityModel, operation_name: str) -> Operation:
        plugin = self.plugins.get_plugin_by_variability_model(src)
        return plugin.get_operation(operation_name)

    def use_operation(self, src: VariabilityModel, operation_name: str) -> Operation:
        plugin = self.plugins.get_plugin_by_variability_model(src)
        operation = plugin.get_operation(operation_name)
        return plugin.use_operation(operation, src)

    # pylint: disable=too-many-arguments
    def use_operation_from_vm(
        self,
        operation_name: str,
        vm_orig: VariabilityModel,
        plugin_name: Optional[str] = None,
        configuration_file: Optional[str] = None,
        is_full: Optional[bool] = False,
    ) -> Any:
        if operation_name not in self.get_name_operations():
            raise OperationNotFound()
        vm_temp = vm_orig
        if plugin_name is not None:
            plugin = self.plugins.get_plugin_by_name(plugin_name)
            # vm_temp = plugin.use_transformation_t2m(file)
        else:
            # vm_temp = self.__transform_to_model_from_file(file)
            plugin = self.plugins.get_plugin_by_extension(vm_orig.get_extension())

            if operation_name not in self.get_name_operations_by_plugin(plugin.name):
                transformation_way = self.__search_transformation_way(plugin, operation_name)

                for _, dst in transformation_way:
                    _plugin = self.plugins.get_plugin_by_extension(dst)
                    vm_temp = _plugin.use_transformation_m2m(vm_temp, dst)
                    plugin = _plugin

        operation = plugin.get_operation(operation_name)
        if isinstance(operation, OperationWithConfiguration):
            if configuration_file is None:
                raise ConfigurationNotFound()
            configuration = self.__transform_to_model_from_file(configuration_file)
            configuration = cast(Configuration, configuration)
            is_full_value = is_full if is_full is not None else False
            configuration.set_full(is_full_value)
            operation.set_configuration(configuration)

        operation = plugin.use_operation(operation, vm_temp)

        return operation.get_result()

    # pylint: disable=too-many-arguments
    def use_operation_from_file(
        self,
        operation_name: str,
        file: str,
        plugin_name: Optional[str] = None,
        configuration_file: Optional[str] = None,
        is_full: Optional[bool] = False,
    ) -> Any:
        if operation_name not in self.get_name_operations():
            raise OperationNotFound()

        if plugin_name is not None:
            plugin = self.plugins.get_plugin_by_name(plugin_name)
            vm_temp = plugin.use_transformation_t2m(file)
        else:
            vm_temp = self.__transform_to_model_from_file(file)
            plugin = self.plugins.get_plugin_by_extension(vm_temp.get_extension())

            if operation_name not in self.get_name_operations_by_plugin(plugin.name):
                transformation_way = self.__search_transformation_way(plugin, operation_name)

                for _, dst in transformation_way:
                    _plugin = self.plugins.get_plugin_by_extension(dst)
                    vm_temp = _plugin.use_transformation_m2m(vm_temp, dst)
                    plugin = _plugin

        operation = plugin.get_operation(operation_name)
        if isinstance(operation, OperationWithConfiguration):
            if configuration_file is None:
                raise ConfigurationNotFound()
            configuration = self.__transform_to_model_from_file(configuration_file)
            configuration = cast(Configuration, configuration)
            is_full_value = is_full if is_full is not None else False
            configuration.set_full(is_full_value)
            operation.set_configuration(configuration)

        operation = plugin.use_operation(operation, vm_temp)
        return operation.get_result()

    def __transform_to_model_from_file(self, file: str) -> VariabilityModel:
        t2m_transformations = self.get_transformations_t2m()
        extension = file.rsplit(".", maxsplit=1)[-1]
        t2m_filters = filter(
            lambda t2m: t2m.get_source_extension() == extension, t2m_transformations
        )

        t2m = next(t2m_filters, None)
        if t2m is None:
            raise TransformationNotFound()

        return t2m(file).transform()

    def __search_transformation_way(
        self, plugin: Plugin, operation_name: str
    ) -> list[tuple[str, str]]:
        """
        Search way to reach plugin with operation_name using m2m transformations
        """
        way: list[tuple[str, str]] = []

        plugins_with_operation = self.get_plugins_with_operation(operation_name)
        m2m_transformations = self.get_transformations_m2m()

        input_extension = plugin.get_extension()

        def __search_recursive_way(
            input_extension: str, output_extension: str, tmp_way: list[tuple[str, str]]
        ) -> list[tuple[str, str]]:
            for m2m in m2m_transformations:
                in_m2m = m2m.get_source_extension()
                out_m2m = m2m.get_destination_extension()

                if out_m2m == output_extension:
                    _next = (in_m2m, out_m2m)

                    if _next in tmp_way:
                        continue

                    tmp_way.insert(0, _next)

                    if input_extension == in_m2m:
                        return tmp_way

                    return __search_recursive_way(input_extension, in_m2m, tmp_way)

            return tmp_way

        for _plugin in plugins_with_operation:
            output_extension = _plugin.get_extension()
            way = __search_recursive_way(input_extension, output_extension, [])
            if way and output_extension == way[-1][1]:
                return way

        raise NotImplementedError("Way to execute operation not found")
