import inspect
import logging
from logging import Logger
from types import ModuleType
from typing import Any, List, Type, Union, Optional, Callable, Set

from dependencyinjection.exception import DependencyInjectionException

from ._DependencyBuilder import DependencyBuilder
from ._DependencyContext import DependencyContext
from ._DependencyContextImpl import DependencyContextImpl
from ._DependencyMap import DependencyMap
from ._utils import (
    load_module_then_execute,
    get_method_by_name,
    inherit_from,
)

from ._types import AnyType


class DependencyLoader:

    def __init__(self, logger: Optional[Logger] = None):
        self.dependency_map = DependencyMap()
        self.logger = logger or logging.getLogger()

    def load_modules(
        self,
        modules: List[ModuleType],
        recursive: Optional[bool] = True,
        class_filter: Optional[Callable[[Type, ], bool]] = None,
    ):
        for module in modules:
            load_module_then_execute(
                load_module=module,
                callback=lambda m: self._load_dependencies_from_module(
                    load_module=m,
                    class_filter=class_filter
                ),
                logger=self.logger,
                recursive=recursive
            )

    def build_context(self) -> DependencyContext:
        return DependencyContextImpl(
            dependency_map=self.dependency_map.clone(),
            logger=self.logger,
        )

    def get_dependencies_by_parent(self, parent_type: Type[AnyType]) -> Set[Type[AnyType]]:
        return self.dependency_map.get_dependencies_by_parent(parent_type)

    def add_dependency_instance(self, dependency_instance: Any):
        self.dependency_map.add_dependency_instance(dependency_instance)
        self.add_dependency_class(type(dependency_instance))

    def add_dependency_class(self, dep_class: Type[AnyType]):
        if not inspect.isabstract(dep_class):
            self.logger.debug("---> Loaded class dependency %s", dep_class.__name__)
            if self._is_builder(dep_class):
                builder_method = self._get_builder_method(dep_class)
                if builder_method is None:
                    raise DependencyInjectionException(f"Builder class {dep_class.__name__} missing build method")
                build_method = inspect.signature(builder_method)
                build_return_cls = build_method.return_annotation
                self.dependency_map.add_builder(
                    builder_cls=dep_class,
                    builder_return_cls=build_return_cls,
                )
            else:
                self.dependency_map.add_class(dep_class)

            self.dependency_map.add_class_by_parent(dep_class)

    def add_dependency_extra_parameter(
        self,
        parameter_name: str,
        parameter_value: Any,
        for_type: Optional[Type] = None
    ):
        self.dependency_map.add_dependency_extra_parameter(
            parameter_name=parameter_name,
            parameter_value=parameter_value,
            for_type=for_type,
        )

    def add_dependency_extra_parameters(
        self,
        parameters: Any,
        for_type: Optional[Type] = None,
    ):
        self.dependency_map.add_dependency_extra_parameters(
            parameters=parameters,
            for_type=for_type,
        )

    def _load_dependencies_from_module(
        self,
        load_module: ModuleType,
        class_filter: Optional[Callable[[Type, ], bool]] = None,
    ):
        for _, member_obj in inspect.getmembers(load_module):
            if inspect.isclass(member_obj) and not inspect.isabstract(member_obj):
                if class_filter is not None and class_filter(member_obj):
                    self.add_dependency_class(member_obj)
                elif class_filter is None:
                    self.add_dependency_class(member_obj)

    @staticmethod
    def _get_builder_method(element: Union[Type, object]) -> Optional[Callable]:
        return get_method_by_name(element, 'build')

    @staticmethod
    def _is_builder(dep_class: Union[Type, object]) -> bool:
        return inherit_from(dep_class, DependencyBuilder)
