import inspect
from logging import Logger
from types import GeneratorType
from typing import Any, Callable, Dict, Optional, Set, Type

from ._DependencyContext import DependencyContext
from ._DependencyMap import DependencyMap
from ._types import AnyType
from ._utils import (
    DEFAULT_LOGGER,
    MISSING,
    cached_isabstract,
    cached_signature,
    get_output_types,
    is_skippable_parameter,
)
from .exception import (
    AbstractDependencyException,
    DependencyInjectionException,
    DependencyNotFoundException,
)


class DependencyContextImpl(DependencyContext):
    """Implementation of DependencyContext for managing and resolving dependencies."""

    def __init__(
        self,
        dependency_map: DependencyMap,
        logger: Optional[Logger] = None,
    ):
        """Initializes the DependencyContextImpl.

        Args:
            dependency_map (DependencyMap): The dependency map to use for resolving dependencies.
            logger (Logger, optional): A logger instance. Defaults to DEFAULT_LOGGER.
        """
        super().__init__(dependency_map=dependency_map)
        self.logger = logger or DEFAULT_LOGGER

        self._dispose_order = []
        self._dispose_set = set()
        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
    ) -> AnyType:
        """Retrieves or creates a single instance of a given dependency class.

        Args:
            instance_cls (Type[AnyType]): The class of the dependency to retrieve or create.
            params (Dict[str, Any], optional): Optional parameters for instance creation. Defaults to None.

        Returns:
            AnyType: The dependency instance.
        """
        return self._get_instance(
            instance_cls=instance_cls,
            params=params,
            external_class=True,
        )

    def _get_instance(
        self,
        instance_cls: Type[AnyType],
        params: Optional[Dict[str, Any]] = None,
        external_class: Optional[bool] = False,
        default_value: Optional[Any] = MISSING,
    ) -> AnyType:
        """Internal method to retrieve or create a single instance of a given dependency class.

        Args:
            instance_cls (Type[AnyType]): The class of the dependency to retrieve or create.
            params (Dict[str, Any], optional): Optional parameters for instance creation. Defaults to None.
            external_class (bool, optional): Internal flag. Defaults to False.
            default_value (Any, optional): Internal flag for default values. Defaults to MISSING.

        Raises:
            DependencyInjectionException: If instance_cls is None.
            AbstractDependencyException: If the dependency is an abstract class or interface.
            DependencyNotFoundException: If the dependency cannot be found or resolved.

        Returns:
            AnyType: The dependency instance.
        """

        if instance_cls is None:
            raise DependencyInjectionException("Instance class cannot be None")

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

        # Fast path: direct lookup before entering the full resolution loop
        instance = self.dependency_map.dependency_instances.get(instance_cls, MISSING)
        if instance is not MISSING:
            return instance

        for type_cls in get_output_types(instance_cls):
            if type_cls is None:
                default_value = None
                continue

            instance = self.dependency_map.dependency_instances.get(type_cls, MISSING)
            if instance is not MISSING:
                return instance

            builder = self.dependency_map.builders.get(type_cls, MISSING)
            if builder is not MISSING:
                builder_instance = None
                if inspect.isfunction(builder):
                    dependency_instance = self.call_function(
                        function=builder,
                        params=params,
                    )

                    if isinstance(dependency_instance, GeneratorType):
                        self._push_dispose(dependency_instance)
                        dependency_instance = next(dependency_instance)

                else:
                    builder_instance = self._create_instance(
                        instance_cls=builder,
                        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)

                if builder_instance is not None:
                    # Ensure mismatch builder method type vs what is returned
                    self.dependency_map.add_builder(
                        builder=type(builder_instance),
                        for_type=dependency_type
                    )

                self._push_dispose(dependency_instance)
                self._push_dispose(builder_instance)

                return dependency_instance

            if external_class and not cached_isabstract(instance_cls):
                dependency_class = instance_cls
            else:
                dependency_class = self.dependency_map.classes.get(type_cls, MISSING)
                if dependency_class is MISSING:
                    dependency_class = None

            if dependency_class is not None:
                dependency_instance = self._create_instance(
                    instance_cls=dependency_class,
                    params=params
                )
                if dependency_instance is not None:
                    self._push_dispose(dependency_instance)
                    return dependency_instance

        if default_value is MISSING:
            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:
        """Internal method to create an instance of a given dependency class.

        Args:
            instance_cls (Type[AnyType]): The class of the dependency to create.
            params (Dict[str, Any], optional): Optional parameters for instance creation. Defaults to None.
            only_create (bool, optional): If True, the instance is created but not added to the context.

        Raises:
            AbstractDependencyException: If the dependency is an abstract class or interface.

        Returns:
            AnyType: The created dependency instance.
        """
        instance = self.dependency_map.dependency_instances.get(instance_cls, MISSING)
        if instance is not MISSING:
            return instance

        if cached_isabstract(instance_cls):
            raise AbstractDependencyException(instance_cls)

        self.logger.debug("Creating instance of %s", instance_cls.__name__)
        factored_constructor_args = self._get_function_parameters(
            function=instance_cls.__init__,
            instance_cls=instance_cls,
            params=params,
        )
        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 call_function(
        self,
        function: Callable[..., AnyType],
        params: Optional[Dict[str, Any]] = None
    ) -> AnyType:
        """Call an function with injected parameters

        Args:
            function (Callable): The function for which to resolve parameters.
            params (Optional[Dict[str, Any]], optional): Additional parameters to consider during resolution. Defaults to None.

        Returns:
            AnyType: The result of the function call.
        """
        function_parameters = self._get_function_parameters(
            function=function,
            instance_cls=None,
            params=params,
        )

        return function(**function_parameters)

    def _get_function_parameters(
        self,
        function: Callable,
        instance_cls: Optional[Type[AnyType]] = None,
        params: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        """Internal implementation of get_function_parameters.

        Resolves and returns parameters for a given function.

        Args:
            function (Callable): The function for which to resolve parameters.
            instance_cls (Type[AnyType], optional): The class of the instance associated with the function. Defaults to None.
            params (Dict[str, Any], optional): Additional parameters to consider during resolution. Defaults to None.

        Returns:
            Dict[str, Any]: A dictionary of resolved parameters.
        """
        factored_constructor_args = {}
        signature = cached_signature(function)
        for parameter in signature.parameters.values():
            if is_skippable_parameter(parameter):
                continue
            parameter_name = parameter.name
            if params is not None and parameter_name in params:
                parameter_value = params[parameter_name]
            else:
                try:
                    param_default = parameter.default
                    if param_default is inspect.Parameter.empty:
                        param_default = MISSING
                    parameter_value = self.get_instance_parameter(
                        instance_cls=instance_cls,
                        parameter_name=parameter_name,
                        parameter_cls=parameter.annotation,
                        default_value=param_default,
                    )
                except DependencyNotFoundException as ex:
                    raise DependencyNotFoundException(
                        dependency_cls=ex.dependency_cls,
                        path=f"{ex.path} -> {ex.dependency_cls}.{parameter_name}"
                    )

            factored_constructor_args[parameter_name] = parameter_value
        return factored_constructor_args

    def get_instance_parameter(
        self,
        parameter_cls: Type[AnyType],
        instance_cls: Optional[Type[AnyType]] = None,
        parameter_name: Optional[str] = None,
        default_value: Optional[Any] = MISSING,
    ):
        """Resolves and returns a single parameter for a function or method.

        Args:
            parameter_cls (Type[AnyType]): The expected class of the parameter.
            instance_cls (Type[AnyType], optional): The class of the instance associated with the parameter. Defaults to None.
            parameter_name (str, optional): The name of the parameter. Defaults to None.
            default_value (Any, optional): The default value for the parameter if it cannot be resolved. Defaults to MISSING.

        Raises:
            DependencyNotFoundException: If the dependency for the parameter cannot be found or resolved.

        Returns:
            Any: The resolved parameter value.
        """
        if parameter_name is not None:
            extra = self.dependency_map.get_extra_parameter(
                parameter_name=parameter_name,
                for_type=instance_cls,
                default=MISSING,
            )
            if extra is not MISSING:
                return extra

        try:
            factored_instance = self._get_instance(
                instance_cls=parameter_cls,
                default_value=default_value,
            )
            return factored_instance
        except DependencyNotFoundException as ex:
            raise DependencyNotFoundException(
                dependency_cls=ex.dependency_cls,
                path=f"{ex.path} -> {ex.dependency_cls}.{parameter_name}"
            )

    def get_instances_by_type(
        self,
        parent_type: Type[AnyType],
    ) -> Set[AnyType]:
        """Retrieves all instances of a given type.

        Args:
            parent_type (Type[AnyType]): The type of the instances to retrieve.

        Returns:
            Set[AnyType]: A set of dependency instances matching the parent type.
        """
        ret = set()
        for dependency_cls in self.dependency_map.get_dependencies_by_parent(parent_type):
            instance = self._get_instance(instance_cls=dependency_cls, default_value=None)
            if instance is not None:
                ret.add(instance)
        return ret

    def _push_dispose(self, instance):
        if instance is not None and id(instance) not in self._dispose_set:
            self._dispose_set.add(id(instance))
            self._dispose_order.append(instance)

    def dispose(self):
        """Disposes of all managed dependency instances in the reverse order of their creation."""
        for instance in reversed(self._dispose_order):
            instance_type = type(instance)
            if isinstance(instance, GeneratorType):
                instance.close()
            else:
                builder_type = self.dependency_map.builders.get(instance_type, MISSING)
                if builder_type is not MISSING:
                    builder_instance = self.dependency_map.dependency_instances.get(builder_type, None)
                    if builder_instance is not None:
                        builder_instance.destroy(instance)
                else:
                    self._check_destroy_method(instance)

    @staticmethod
    def _check_created_method(dependency_instance) -> bool:
        enter = getattr(dependency_instance, '__enter__', None)
        if enter is not None and callable(enter):
            enter()
            return True
        return False

    @staticmethod
    def _check_destroy_method(instance) -> bool:
        exit_ = getattr(instance, '__exit__', None)
        if exit_ is not None and callable(exit_):
            exit_(None, None, None)
            return True
        return False
