import inspect
import logging
from logging import Logger
from typing import (
    Type,
    TypeVar,
    Any,
    Optional,
    Dict,
    Set,
    Union,
    get_args
)

from ._DependencyContext import DependencyContext
from ._DependencyMap import DependencyMap
from ._utils import (
    get_class_parent_types
)
from .exception import DependencyNotFoundException, AbstractDependencyException

AnyType = TypeVar("AnyType")


class DependencyContextImpl(DependencyContext):

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

        self.dependency_instances = {}
        self.dependency_extra_parameters: Dict[AnyType, Dict[str, Any]] = {}
        self.logger = logger or logging.getLogger()
        self._dispose_order = []
        self.add_dependency_instance(self)
        self.add_dependency_instance(self.logger)

    def add_dependency_instance(self, dependency_instance: Any):
        self.dependency_instances[type(dependency_instance)] = dependency_instance
        for base_class in get_class_parent_types(type(dependency_instance)):
            self.dependency_instances[base_class] = dependency_instance

    def add_dependency_extra_parameter(
        self,
        parameter_name: str,
        parameter_value: Any,
        for_type: Optional[AnyType] = None
    ):
        if for_type not in self.dependency_extra_parameters:
            self.dependency_extra_parameters[for_type] = {}
        self.dependency_extra_parameters[for_type][parameter_name] = parameter_value

    def get_instance(
        self,
        cls: Type[AnyType],
        params: Optional[Dict[str, Any]] = None
    ) -> AnyType:
        return self._get_instance(
            cls,
            params=params,
            ignore_in_map=True,
            is_optional=False
        )

    def _get_instance(
        self,
        cls: Type[AnyType],
        params: Optional[Dict[str, Any]] = None,
        ignore_in_map: Optional[bool] = False,
        is_optional: Optional[bool] = False,
    ) -> AnyType:
        if cls is None:
            return None

        try:
            if isinstance(self, cls):
                return self
        except TypeError:
            pass

        if cls in self.dependency_instances:
            return self.dependency_instances[cls]

        if cls in self._dependency_map.builders:
            builder_instance = self._create_instance(
                cls=self._dependency_map.builders[cls],
                params=params
            )
            dependency_instance = builder_instance.build()
            dependency_type = type(dependency_instance)
            self.logger.debug(f"Built instance of {dependency_type.__name__}")
            self.add_dependency_instance(dependency_instance)
            if dependency_type not in self._dependency_map.builders:
                self._dependency_map.builders[dependency_type] = self._dependency_map.builders[cls]
                for child_type in get_class_parent_types(dependency_type):
                    self._dependency_map.builders[child_type] = self._dependency_map.builders[cls]

            self._dispose_order.append(dependency_instance)
            self._dispose_order.append(builder_instance)

            return dependency_instance

        dependency_class = None
        if ignore_in_map and not inspect.isabstract(cls):
            dependency_class = cls
        elif cls in self._dependency_map.classes:
            dependency_class = self._dependency_map.classes[cls]

        if dependency_class is not None:
            dependency_instance = self._create_instance(
                cls=dependency_class,
                params=params
            )
            if dependency_instance is not None:
                self._dispose_order.append(dependency_instance)
                return dependency_instance

        if cls in self._dependency_map.classes:
            dependency_class = self._dependency_map.classes[cls]
            dependency_instance = self._create_instance(
                cls=dependency_class,
                params=params
            )
            self._dispose_order.append(dependency_instance)
            return dependency_instance

        if not is_optional:
            raise DependencyNotFoundException(cls)

        return None

    def get_dependencies_by_parent(self, parent_type: Type[AnyType]) -> Set[Type[AnyType]]:
        return self._dependency_map.classes_by_parent.get(parent_type, set())

    def has_instance(self, cls: Type):
        return cls in self.dependency_instances

    def _create_instance(
        self,
        cls: Type,
        params: Optional[Dict[str, Any]] = None,
        only_create: Optional[bool] = False
    ):
        if cls in self.dependency_instances:
            return self.dependency_instances[cls]

        if inspect.isabstract(cls):
            raise AbstractDependencyException(cls)

        self.logger.debug(f"Creating instance of {cls.__name__}")
        signature = inspect.signature(cls.__init__)
        factored_constructor_args = {}
        for parameter in signature.parameters.values():
            if parameter.name in ["self", "args", "kwargs"]:
                continue

            if params is not None and parameter.name in params:
                factored_constructor_args[parameter.name] = params[parameter.name]
            elif cls in self.dependency_extra_parameters and parameter.name in self.dependency_extra_parameters[cls]:
                factored_constructor_args[parameter.name] = self.dependency_extra_parameters[cls][parameter.name]
            elif None in self.dependency_extra_parameters and parameter.name in self.dependency_extra_parameters[None]:
                factored_constructor_args[parameter.name] = self.dependency_extra_parameters[None][parameter.name]
            else:
                build_cls = parameter.annotation
                # Validation for parameters Optional[some_type]
                if getattr(build_cls, "__origin__", None) == Union:
                    args = get_args(build_cls)
                    if len(args) > 0:
                        build_cls = get_args(build_cls)[0]
                    else:
                        build_cls = None

                try:
                    factored_constructor_args[parameter.name] = self._get_instance(
                        cls=build_cls,
                        is_optional=not (parameter.default is parameter.empty)
                    )
                except DependencyNotFoundException as ex:
                    raise DependencyNotFoundException(ex.cls, f"{ex.path} -> {ex.cls}.{parameter.name}")

        dependency_instance = cls(**factored_constructor_args)
        self._check_created_method(dependency_instance)

        if not only_create:
            self.add_dependency_instance(dependency_instance)

        return dependency_instance

    def dispose(self):
        destroyed_instances = set()
        for instance in self._dispose_order:
            if instance not in destroyed_instances:
                instance_type = type(instance)
                if instance_type in self._dependency_map.builders:
                    builder_type = self._dependency_map.builders[instance_type]
                    builder_instance = self.dependency_instances.get(builder_type, None)
                    if builder_instance is not None:
                        builder_instance.destroy(instance)
                        destroyed_instances.add(instance)
                elif self._check_destroy_method(instance):
                    destroyed_instances.add(instance)

    def _check_created_method(self, dependency_instance):
        if hasattr(dependency_instance, '__enter__') and callable(getattr(dependency_instance, '__enter__')):
            self.logger.debug(f"Created instance of {type(dependency_instance).__name__}")
            dependency_instance.__enter__()
            return True
        return False

    def _check_destroy_method(self, instance):
        if hasattr(instance, '__exit__') and callable(getattr(instance, '__exit__')):
            self.logger.debug(f"Destroying instance of {type(instance).__name__}")
            instance.__exit__(exc_type=None, exc_val=None, exc_tb=None)
            return True
        return False
