import inspect
from collections.abc import Generator
from typing import Any, Callable, Dict, Optional, Set, Type, Union, get_args

from ._DependencyBuilder import DependencyBuilder
from ._DependencyManagerBase import DependencyManagerBase, BuilderType
from ._types import AnyType
from ._utils import (
    cached_isabstract,
    cached_signature,
    get_class_parent_types,
    get_method_by_name,
    get_origin,
    get_output_types,
    inherit_from,
)
from .exception import DependencyInjectionException


class DependencyMap(DependencyManagerBase):
    """A data structure for storing and managing dependency mappings.

    This class holds the relationships between abstract types, concrete classes,
    builders, instances, and extra parameters needed for dependency resolution.
    It acts as the central registry for the dependency injection framework.
    """

    def __init__(
        self,
        classes: Optional[Dict[Type[AnyType], Type[AnyType]]] = None,
        classes_by_parent: Optional[Dict[Type, Set[Type]]] = None,
        builders: Optional[Dict[Type, BuilderType]] = None,
        extra_parameters: Optional[Dict[Optional[Type], Dict[str, Any]]] = None,
        dependency_instances: Optional[Dict[Type[AnyType], AnyType]] = None
    ):
        """Initializes the DependencyMap.

        Args:
            classes (Optional[Dict[Type[AnyType], Type[AnyType]]]): A dictionary mapping abstract types to concrete types.
            classes_by_parent (Optional[Dict[Type, Set[Type]]]): A dictionary mapping parent types to a set of their registered child classes.
            builders (Optional[Dict[Type, BuilderType]]): A dictionary mapping types to their custom builder classes or functions.
            extra_parameters (Optional[Dict[Optional[Type], Dict[str, Any]]]): A dictionary for storing extra parameters for instantiation, keyed by type.
            dependency_instances (Optional[Dict[Type[AnyType], AnyType]]): A dictionary of pre-existing singleton instances.
        """
        self.classes = classes or {}
        self.classes_by_parent = classes_by_parent or {}
        self.builders = builders or {}
        self.extra_parameters = extra_parameters or {}
        self.dependency_instances = dependency_instances or {}

    def add_dependency_instance(self, dependency_instance: Any):
        """Adds a pre-existing instance to the map.

        The instance is registered for its own type and all its parent types.
        Its class is also registered as a dependency.

        Args:
            dependency_instance (Any): The instance to add.
        """
        dependency_type = type(dependency_instance)
        self.dependency_instances[dependency_type] = dependency_instance
        for base_class in get_class_parent_types(dependency_type):
            self.dependency_instances[base_class] = dependency_instance

        self.add_dependency_class(dependency_type)

    def has_instance(self, instance_cls: Type) -> bool:
        """Checks if an instance of a specific class is present in the map.

        Args:
            instance_cls (Type): The class type to check for.

        Returns:
            bool: True if an instance exists, False otherwise.
        """
        return instance_cls in self.dependency_instances

    def get_instance(self, instance_cls: Type[AnyType]) -> AnyType:
        """Retrieves a pre-existing instance of a specific class from the map.

        Args:
            instance_cls (Type[AnyType]): The class type of the instance to retrieve.

        Returns:
            AnyType: The stored instance.
        """
        return self.dependency_instances[instance_cls]

    def get_dependencies_by_parent(self, parent_type: Type[AnyType]) -> Set[Type[AnyType]]:
        """Gets all registered dependency classes that are subclasses of a given parent type.

        Args:
            parent_type (Type[AnyType]): The parent type to search for.

        Returns:
            Set[Type[AnyType]]: A set of dependency types that inherit from the parent type.
        """
        return self.classes_by_parent.get(parent_type, frozenset())

    def add_dependency_extra_parameter(
        self,
        parameter_name: str,
        parameter_value: Any,
        for_type: Optional[Type] = None
    ):
        """Adds an extra parameter for dependency instantiation.

        Args:
            parameter_name (str): The name of the parameter.
            parameter_value (Any): The value of the parameter.
            for_type (Optional[Type]): The specific type this parameter is for. If None, it's a global parameter.
        """
        if for_type not in self.extra_parameters:
            self.extra_parameters[for_type] = {}
        self.extra_parameters[for_type][parameter_name] = parameter_value

    def has_extra_parameter(self, parameter_name: str, for_type: Optional[Type] = None) -> bool:
        """Checks if an extra parameter is defined, either for a specific type or globally.

        Args:
            parameter_name (str): The name of the parameter.
            for_type (Optional[Type]): The specific type the parameter might be for.

        Returns:
            bool: True if the parameter is defined, False otherwise.
        """
        type_params = self.extra_parameters.get(for_type)
        if type_params is not None and parameter_name in type_params:
            return True
        global_params = self.extra_parameters.get(None)
        if global_params is not None and parameter_name in global_params:
            return True
        return False

    def get_extra_parameter(self, parameter_name: str, for_type: Optional[Type] = None, default: Any = None) -> Any:
        """Gets the value of an extra parameter.

        It first checks for a type-specific parameter, then falls back to a global one.

        Args:
            parameter_name (str): The name of the parameter.
            for_type (Optional[Type]): The specific type the parameter is for.
            default (Any): The default value to return if not found. Defaults to None.

        Returns:
            Any: The value of the parameter, or default if not found.
        """
        type_params = self.extra_parameters.get(for_type)
        if type_params is not None and parameter_name in type_params:
            return type_params[parameter_name]
        global_params = self.extra_parameters.get(None)
        if global_params is not None and parameter_name in global_params:
            return global_params[parameter_name]
        return default

    def has_dependency_class(self, dependency_cls: Type) -> bool:
        """Checks if a dependency class is registered.

        Args:
            dependency_cls (Type): The class to check.

        Returns:
            bool: True if the class is registered, False otherwise.
        """
        return dependency_cls in self.classes

    def get_dependency_class(self, dependency_cls: Type[AnyType]) -> Type[AnyType]:
        """Gets a registered dependency class.

        Args:
            dependency_cls (Type[AnyType]): The class to retrieve.

        Returns:
            Type[AnyType]: The registered dependency class.
        """
        return self.classes[dependency_cls]

    def has_dependency_builder(self, builder_cls: Type) -> bool:
        """Checks if a builder is registered for a specific type.

        Args:
            builder_cls (Type): The type to check for a builder.

        Returns:
            bool: True if a builder is registered, False otherwise.
        """
        return builder_cls in self.builders

    def get_dependency_builder(self, builder_cls: Type[AnyType]) -> BuilderType:
        """Gets the builder registered for a specific type.

        Args:
            builder_cls (Type[AnyType]): The type whose builder is to be retrieved.

        Returns:
            BuilderType: The registered builder class or callable.
        """
        return self.builders[builder_cls]

    def add_dependency_extra_parameters(
        self,
        parameters: Any,
        for_type: Optional[Type] = None,
    ):
        """Adds multiple extra parameters from a dictionary or object.

        Args:
            parameters (Any): A dictionary or object with parameter names and values.
            for_type (Optional[Type]): The specific type these parameters are for. If None, they are global.
        """
        if parameters is None:
            return
        parameters_dict = parameters
        if not isinstance(parameters_dict, dict) and hasattr(parameters_dict, '__dict__'):
            parameters_dict = parameters_dict.__dict__
        if isinstance(parameters_dict, dict):
            for parameter_name, parameter_value in parameters_dict.items():
                self.add_dependency_extra_parameter(
                    parameter_name=parameter_name,
                    parameter_value=parameter_value,
                    for_type=for_type,
                )

    def add_dependency_class(self, dep_cls: Type[AnyType]):
        """Registers a class as a dependency.

        If the class is a `DependencyBuilder`, it's added as a builder.
        Otherwise, it's registered as a standard dependency class.

        Args:
            dep_cls (Type[AnyType]): The dependency class to register.
        """
        if not cached_isabstract(dep_cls):
            parent_types = get_class_parent_types(dep_cls)
            if self._is_builder(dep_cls):
                self.add_builder(builder=dep_cls)
            else:
                self._add_class(dep_cls, parent_types)
            self._add_class_by_parent(dep_cls, parent_types)

    def _add_class(self, dep_cls: Type[AnyType], parent_types: list):
        """Internal method to register a class and its parents in the `classes` map.

        Args:
            dep_cls (Type[AnyType]): The concrete class to add.
            parent_types (list): Pre-computed parent types for the class.
        """
        if dep_cls not in self.classes:
            self.classes[dep_cls] = dep_cls
            for parent in parent_types:
                self.classes[parent] = dep_cls

    def _add_class_by_parent(self, dep_cls: Type[AnyType], parent_types: list):
        """Internal method to map a class to its parents in the `classes_by_parent` map.

        This is used for retrieving all implementations of a specific base class/interface.

        Args:
            dep_cls (Type[AnyType]): The class to map.
            parent_types (list): Pre-computed parent types for the class.
        """
        for base_type in parent_types:
            existing = self.classes_by_parent.get(base_type, frozenset())
            self.classes_by_parent[base_type] = existing | {dep_cls}

    def add_builder(
        self,
        builder: BuilderType,
        for_type: Optional[Type[AnyType]] = None
    ):
        """Adds a custom builder for a specific type.

        The type can be specified explicitly or inferred from the builder's `build` method return type hint.

        Args:
            builder (BuilderType): The builder class or callable function.
            for_type (Optional[Type[AnyType]]): The type this builder is responsible for creating.

        Raises:
            DependencyInjectionException: If the return type of the builder cannot be determined.
        """
        if for_type is None:
            if inspect.isfunction(builder):
                builder_method = builder
            else:
                builder_method = self._get_builder_method(builder)
                if builder_method is None:
                    raise DependencyInjectionException(f"Builder class {builder.__name__} missing build method")

            build_method_signature = cached_signature(builder_method)
            for_type = build_method_signature.return_annotation
            if get_origin(for_type) is Generator:
                args = get_args(for_type)
                for_type = args[0]

        builder_output_types = get_output_types(for_type)
        builder_output_types = [t for t in builder_output_types if t is not None]

        if len(builder_output_types) == 0:
            raise DependencyInjectionException(
                f"Cannot determine return type for builder {builder.__module__}.{builder.__name__}")

        for builder_output_type in builder_output_types:
            self.builders[builder_output_type] = builder
            if inspect.isclass(builder):
                for child_type in get_class_parent_types(builder_output_type):
                    self.builders[child_type] = builder

    @staticmethod
    def _get_builder_method(element: Union[Type, object]) -> Optional[Callable]:
        """Retrieves the 'build' method from a builder class.

        Args:
            element (Union[Type, object]): The builder class or instance.

        Returns:
            Optional[Callable]: The 'build' method, or None if not found.
        """
        return get_method_by_name(element, 'build')

    @staticmethod
    def _is_builder(dep_class: Union[Type, object]) -> bool:
        """Checks if a class is a subclass of DependencyBuilder.

        Args:
            dep_class (Union[Type, object]): The class or object to check.

        Returns:
            bool: True if it inherits from DependencyBuilder, False otherwise.
        """
        return inherit_from(dep_class, DependencyBuilder)

    def clone(self):
        """Creates a shallow copy of the DependencyMap.

        This is useful for creating new contexts (e.g., for a request)
        based on a pre-configured loader map, without affecting the original.

        Returns:
            DependencyMap: A new DependencyMap instance with copied dictionaries.
        """
        return DependencyMap(
            classes={**self.classes},
            classes_by_parent={**self.classes_by_parent},
            builders={**self.builders},
            extra_parameters={k: {**v} for k, v in self.extra_parameters.items()},
            dependency_instances={**self.dependency_instances},
        )
