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

from ._DependencyContext import DependencyContext
from ._DependencyMap import DependencyMap
from ._types import AnyType
from .exception import AbstractDependencyException, DependencyNotFoundException


class DependencyContextImpl(DependencyContext):

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

        self._dispose_order = []
        self.add_dependency_instance(self)
        self.add_dependency_instance(self.logger)

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

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

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

        if self.dependency_map.has_instance(instance_cls):
            return self.dependency_map.get_instance(instance_cls)

        if self.dependency_map.has_dependency_builder(instance_cls):
            builder_instance = self._create_instance(
                instance_cls=self.dependency_map.get_dependency_builder(instance_cls),
                params=params
            )
            dependency_instance = builder_instance.build()
            dependency_type = type(dependency_instance)
            self.logger.debug("Built instance of %s", dependency_type.__name__)
            self.add_dependency_instance(dependency_instance)

            # Ensure mismatch builder method type vs what is returned
            self.dependency_map.add_builder(
                builder_cls=type(builder_instance),
                builder_return_cls=dependency_type
            )

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

            return dependency_instance

        if ignore_in_map and not inspect.isabstract(instance_cls):
            dependency_class = instance_cls
            dependency_instance = self._create_instance(
                instance_cls=dependency_class,
                params=params
            )
            if dependency_instance is not None:
                self._dispose_order.append(dependency_instance)
                return dependency_instance

        if self.dependency_map.has_dependency_class(instance_cls):
            dependency_class = self.dependency_map.get_dependency_class(instance_cls)
            dependency_instance = self._create_instance(
                instance_cls=dependency_class,
                params=params
            )
            self._dispose_order.append(dependency_instance)
            return dependency_instance

        if not is_optional:
            raise DependencyNotFoundException(instance_cls)

        return default_value

    def _create_instance(
        self,
        instance_cls: Type[AnyType],
        params: Optional[Dict[str, Any]] = None,
        only_create: Optional[bool] = False
    ) -> AnyType:
        if self.dependency_map.has_instance(instance_cls):
            return self.dependency_map.get_instance(instance_cls)

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

        self.logger.debug("Creating instance of %s", instance_cls.__name__)
        signature = inspect.signature(instance_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 self.dependency_map.has_extra_parameter(parameter_name=parameter.name, for_type=instance_cls):
                factored_constructor_args[parameter.name] = self.dependency_map.get_extra_parameter(
                    parameter_name=parameter.name,
                    for_type=instance_cls
                )
            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:
                    default_value = None
                    if not (parameter.default is parameter.empty):
                        default_value = parameter.default
                    factored_instance = self._get_instance(
                        instance_cls=build_cls,
                        is_optional=not (parameter.default is parameter.empty),
                        default_value=default_value,
                    )
                    factored_constructor_args[parameter.name] = factored_instance
                except DependencyNotFoundException as ex:
                    raise DependencyNotFoundException(
                        dependency_cls=ex.dependency_cls,
                        path=f"{ex.path} -> {ex.dependency_cls}.{parameter.name}"
                    )

        dependency_instance = 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 get_dependencies_by_parent(self, parent_type: Type[AnyType]) -> Set[Type[AnyType]]:
        return self.dependency_map.get_dependencies_by_parent(parent_type)

    def get_instances_by_type(
        self,
        parent_type: Type[AnyType],
        is_optional: Optional[bool] = False,
    ) -> Set[AnyType]:
        ret = set()
        for dependency_cls in self.get_dependencies_by_parent(parent_type) or {}:
            instance = self._get_instance(instance_cls=dependency_cls, is_optional=is_optional)
            if instance is not None:
                ret.add(instance)
        return ret

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

    def has_instance(self, instance_cls: Type) -> bool:
        return self.dependency_map.has_instance(instance_cls)

    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 dispose(self):
        destroyed_instances = set()
        for instance in self._dispose_order:
            if instance not in destroyed_instances:
                instance_type = type(instance)
                if self.dependency_map.has_dependency_builder(instance_type):
                    builder_type = self.dependency_map.get_dependency_builder(instance_type)
                    if self.dependency_map.has_instance(builder_type):
                        builder_instance = self.dependency_map.get_instance(builder_type)
                        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("Created instance of %s", 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("Destroying instance of %s", type(instance).__name__)
            instance.__exit__(exc_type=None, exc_val=None, exc_tb=None)
            return True
        return False
