import importlib
import inspect
import os
import pkgutil
import sys
from abc import ABC
from logging import getLogger
from types import ModuleType
from typing import (
    Any,
    Callable,
    List,
    Optional,
    Type,
    Union,
    Set,
    get_args,
    get_origin as typing_get_origin
)

UnionType = Union
try:
    from types import UnionType # type: ignore
except ImportError:
    ...


from ._types import AnyType

DEFAULT_LOGGER = getLogger(f"edpdependencyinjection")


def get_method_by_name(element: Union[Type, object], name: str) -> Optional[Callable]:
    """Retrieves a method or function from a class or object by its name.

    Args:
        element (Union[Type, object]): The class or object to inspect.
        name (str): The name of the method to find.

    Returns:
        Optional[Callable]: The found callable method/function, or None if not found.
    """
    for method_name, method_obj in inspect.getmembers(element):
        if (
            (inspect.isfunction(method_obj) or inspect.ismethod(method_obj))
            and method_name == name
        ):
            return method_obj
    return None


def inherit_from(element: Union[Type, object], parent_type: Type) -> bool:
    """Checks if a class or object inherits from a given parent type.

    This handles both instances and class types.

    Args:
        element (Union[Type, object]): The class or object to check.
        parent_type (Type): The parent type to check against.

    Returns:
        bool: True if it inherits from the parent type, False otherwise.
    """
    return (
        isinstance(element, parent_type)
        or (isinstance(element, type) and parent_type in element.__bases__)
    )


def load_module_then_execute(
    load_module: ModuleType,
    callback: Callable[[ModuleType, ], Any],
    recursive: Optional[bool] = True
):
    """Executes a callback on a module and, optionally, on all its sub-modules recursively.

    Args:
        load_module (ModuleType): The root module to start the scan from.
        callback (Callable[[ModuleType, ], Any]): The function to execute for each discovered module.
        recursive (Optional[bool]): If True, scans sub-packages recursively. Defaults to True.
    """
    if recursive:
        package_dir = os.path.dirname(str(load_module.__file__))

        for (_, module_name, _) in pkgutil.iter_modules([package_dir]):
            if os.path.exists(os.path.join(package_dir, module_name, '__init__.py')):
                full_package_name = f"{load_module.__name__}.{module_name}"
                if full_package_name not in sys.modules:
                    module = importlib.import_module(full_package_name)
                else:
                    module = sys.modules[full_package_name]

                load_module_then_execute(module, callback)

    callback(load_module)


def get_class_parent_types(dep_class: Type[AnyType]) -> List[Type[AnyType]]:
    """Gets the direct parent classes of a given class.

    This function filters out common base objects like `ABC` and `object`
    to return only meaningful parent types for dependency resolution.

    Args:
        dep_class (Type[AnyType]): The class to inspect.

    Returns:
        List[Type[AnyType]]: A list of its direct, meaningful parent types.
    """
    return [child for child in dep_class.__bases__ if child not in [ABC, object]]

def get_origin(output_cls):
    if hasattr(output_cls, '__origin__'):
        return typing_get_origin(output_cls)
    return output_cls


def get_output_types(output_cls: Optional[Type[AnyType]]) -> Set[Optional[Type[AnyType]]]:
    """Resolves a type hint into a set of its constituent types.

    This is particularly useful for handling `Union` and `Optional` type hints.
    For `Union[A, B]`, it returns `{A, B}`. For `Optional[A]`, it returns `{A, None}`.
    """
    types_to_check = set()
    type_check = get_origin(output_cls)

    if type(type_check) == UnionType or type_check is Union:
        args = get_args(output_cls)
        for arg in args:
            if arg is type(None):
                types_to_check.add(None)
            else:
                types_to_check.add(arg)
    else:
        types_to_check.add(type_check)

    return types_to_check


