import functools
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,
    FrozenSet,
    Optional,
    Type,
    Union,
    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")
MISSING = object()
_EXCLUDED_BASES = (ABC, object)
_SKIP_PARAM_KINDS = frozenset({inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD})


@functools.lru_cache(maxsize=None)
def cached_signature(function: Callable) -> inspect.Signature:
    return inspect.signature(function)


@functools.lru_cache(maxsize=None)
def cached_isabstract(cls: Type) -> bool:
    return inspect.isabstract(cls)


def is_skippable_parameter(parameter: inspect.Parameter) -> bool:
    return parameter.name == "self" or parameter.kind in _SKIP_PARAM_KINDS


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.
    """
    method_obj = getattr(element, name, None)
    if method_obj is not None and (inspect.isfunction(method_obj) or inspect.ismethod(method_obj)):
        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.
    """
    try:
        cls = element if isinstance(element, type) else type(element)
        return issubclass(cls, parent_type) and cls is not parent_type
    except TypeError:
        return False


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, is_pkg) in pkgutil.iter_modules([package_dir]):
            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]

            if is_pkg:
                load_module_then_execute(module, callback)
            else:
                callback(module)

    callback(load_module)


@functools.lru_cache(maxsize=None)
def get_class_parent_types(dep_class: Type[AnyType]) -> tuple:
    """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:
        tuple: A tuple of its direct, meaningful parent types.
    """
    return tuple(child for child in dep_class.__bases__ if child not in _EXCLUDED_BASES)


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


@functools.lru_cache(maxsize=None)
def get_output_types(output_cls: Optional[Type[AnyType]]) -> FrozenSet[Optional[Type[AnyType]]]:
    """Resolves a type hint into a frozenset of its constituent types.

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

    if type(type_check) == UnionType or type_check is Union:
        args = get_args(output_cls)
        return frozenset(None if arg is type(None) else arg for arg in args)

    return frozenset((output_cls,))
