"""Debug instrumentation utilities for conditional function call tracing.

The helpers in this module are intentionally decoupled from the rest of the
codebase so that debugging aids can be injected without polluting the primary
implementation modules. Instrumentation works by wrapping callables at runtime
to emit structured debug logs whenever they are invoked. This approach keeps
the hot path lean when tracing is disabled while still providing rich insight
into the execution flow during troubleshooting sessions.
"""

from __future__ import annotations

import inspect
import logging
from functools import wraps
from types import ModuleType
from typing import Iterable, Callable, Any

_MARKER_ATTRIBUTE = "__kb_instrumented__"


def _wrap_callable(
    func: Callable[..., Any],
    logger: logging.Logger,
    level: int,
) -> Callable[..., Any]:
    """Return ``func`` wrapped so that calls emit a debug log entry.

    Parameters
    ----------
    func:
        Original callable that should be instrumented.
    logger:
        Logger instance used to emit the tracing messages.
    level:
        Logging level used for the emitted messages. Callers typically pass
        :data:`logging.DEBUG` but the helper accepts any numeric level to keep
        the utility flexible.
    """

    if getattr(func, _MARKER_ATTRIBUTE, False):
        return func

    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        logger.log(level, "Calling %s.%s", func.__module__, func.__qualname__)
        return func(*args, **kwargs)

    setattr(wrapper, _MARKER_ATTRIBUTE, True)
    return wrapper


def _instrument_class(
    cls: type,
    logger: logging.Logger,
    level: int,
    include_private: bool,
) -> None:
    """Instrument methods defined directly on ``cls`` in-place."""

    for name, attribute in vars(cls).items():
        if not include_private and name.startswith("_"):
            continue
        if inspect.isfunction(attribute):
            setattr(cls, name, _wrap_callable(attribute, logger, level))
        elif isinstance(attribute, staticmethod):
            original = attribute.__func__
            wrapped = _wrap_callable(original, logger, level)
            setattr(cls, name, staticmethod(wrapped))
        elif isinstance(attribute, classmethod):
            original = attribute.__func__
            wrapped = _wrap_callable(original, logger, level)
            setattr(cls, name, classmethod(wrapped))


def instrument_modules(
    modules: Iterable[ModuleType],
    *,
    logger: logging.Logger | None = None,
    level: int = logging.DEBUG,
    include_private: bool = False,
) -> None:
    """Instrument ``modules`` so that every function call emits a debug log.

    Parameters
    ----------
    modules:
        Iterable of module objects whose top-level functions and class methods
        should be wrapped with logging instrumentation.
    logger:
        Logger used to emit tracing statements. When omitted, a module-specific
        logger is created automatically.
    level:
        Logging level for the emitted messages. Defaults to ``logging.DEBUG``.
    include_private:
        When ``True``, attributes whose names begin with an underscore are also
        instrumented. The default keeps helper functions private unless
        explicitly requested.
    """

    active_logger = logger or logging.getLogger(__name__)
    for module in modules:
        for name, attribute in vars(module).items():
            if not include_private and name.startswith("_"):
                continue
            if inspect.isfunction(attribute):
                setattr(module, name, _wrap_callable(attribute, active_logger, level))
            elif inspect.isclass(attribute):
                _instrument_class(attribute, active_logger, level, include_private)
