"""Tool handler auto-discovery and loading.

Loads Python handler callables from .py files that sit alongside
tool .yaml config files. Supports two conventions:

1. Simple handler: module exports a ``handler`` callable
2. Factory handler: module exports ``create_handler(**context) -> Callable``

The factory pattern is used when handlers need runtime context (e.g.,
a SandboxSession for sandboxed shell execution).
"""

from __future__ import annotations

import importlib.util
import inspect
from collections.abc import Callable
from pathlib import Path
from typing import Any

from fluxibly.logging import Logger

logger = Logger(component="tool_loader")


def find_handler_path(yaml_path: Path) -> Path | None:
    """Check if a .py handler exists alongside a .yaml tool config.

    Looks for the same filename with ``.py`` extension in the same directory.
    For example: ``tools/file_stats.yaml`` → ``tools/file_stats.py``

    Args:
        yaml_path: Path to the tool YAML config file.

    Returns:
        Path to the sibling ``.py`` file, or ``None`` if not found.
    """
    py_path = yaml_path.with_suffix(".py")
    if py_path.exists():
        return py_path
    return None


def load_handler_module(py_path: Path) -> Any:
    """Dynamically load a Python module from a file path.

    Uses ``importlib.util`` to avoid requiring the file to be on
    ``sys.path`` or inside a Python package.  Each load gets a unique
    module name to prevent collisions when multiple tools share the
    same filename stem in different directories.

    Args:
        py_path: Path to the ``.py`` handler file.

    Returns:
        The loaded module object.

    Raises:
        ImportError: If the module cannot be loaded.
    """
    module_name = f"fluxibly_tool_handler_{py_path.stem}_{id(py_path)}"
    spec = importlib.util.spec_from_file_location(module_name, str(py_path))
    if spec is None or spec.loader is None:
        raise ImportError(f"Cannot load handler module: {py_path}")

    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


async def resolve_handler(
    py_path: Path,
    context: dict[str, Any] | None = None,
) -> Callable[..., Any]:
    """Load and resolve a handler callable from a ``.py`` file.

    Resolution order:

    1. If the module has ``create_handler`` → call it with ``**context``
       → return the resulting callable
    2. If the module has ``handler`` → return it directly
    3. Otherwise → raise ``ValueError``

    The ``create_handler`` factory can be sync or async.

    Args:
        py_path: Path to the ``.py`` handler file.
        context: Optional context dict passed as keyword arguments to
            ``create_handler()``.  Common keys: ``session``
            (SandboxSession).

    Returns:
        The resolved callable (sync or async function).

    Raises:
        ImportError: If the module cannot be loaded.
        ValueError: If the module exports neither ``create_handler``
            nor ``handler``.
        TypeError: If the exported symbol is not callable.
    """
    module = load_handler_module(py_path)
    context = context or {}

    # Option 1: Factory pattern
    if hasattr(module, "create_handler"):
        factory = module.create_handler
        if inspect.iscoroutinefunction(factory):
            handler = await factory(**context)
        else:
            handler = factory(**context)

        if not callable(handler):
            raise TypeError(
                f"create_handler() in {py_path} must return a callable, "
                f"got {type(handler)}"
            )
        logger.debug(
            "Loaded handler via create_handler(): {path}",
            path=str(py_path),
        )
        return handler

    # Option 2: Direct handler
    if hasattr(module, "handler"):
        handler = module.handler
        if not callable(handler):
            raise TypeError(
                f"'handler' in {py_path} must be callable, "
                f"got {type(handler)}"
            )
        logger.debug("Loaded direct handler: {path}", path=str(py_path))
        return handler

    raise ValueError(
        f"Tool handler file {py_path} must export either "
        f"'create_handler(**context) -> Callable' or 'handler' callable. "
        f"Found attributes: {[a for a in dir(module) if not a.startswith('_')]}"
    )
