"""DMS hook registry.

Provides the global registry that maps hook names to lists of callable
handlers, plus the public API used by the service layer and third-party code.

Thread safety
-------------
The registry is a simple in-process ``defaultdict``.  It is populated at
app startup (via ``AppConfig.ready`` or module imports) and is effectively
read-only during request handling, so no locking is required.
"""

from __future__ import annotations

import structlog
from collections import defaultdict
from typing import Callable

_log = structlog.get_logger()

# str → list[Callable[..., None]]
_REGISTRY: dict[str, list[Callable]] = defaultdict(list)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def register_hook(hook_name: str) -> Callable:
    """Decorator that registers *fn* as a handler for *hook_name*.

    The decorated function is returned unchanged so it can still be
    referenced / tested directly.

    Example::

        @register_hook(HOOK_POST_DOCUMENT_CREATE)
        def my_handler(*, document, **kwargs):
            ...
    """
    def decorator(fn: Callable) -> Callable:
        _REGISTRY[hook_name].append(fn)
        return fn

    return decorator


def fire_hook(hook_name: str, **kwargs: object) -> None:
    """Invoke all handlers registered for *hook_name*.

    Handlers are called in registration order.  If a handler raises an
    exception, the error is logged and the remaining handlers are still
    called (best-effort semantics).  The caller never receives the exception.
    """
    for fn in list(_REGISTRY.get(hook_name, [])):
        try:
            fn(**kwargs)
        except Exception:
            _log.warning(
                "dms.hook.error",
                hook=hook_name,
                handler=getattr(fn, "__qualname__", repr(fn)),
                exc_info=True,
            )


def clear_hooks(hook_name: str) -> None:
    """Remove all registered handlers for *hook_name*.

    Intended for use in tests that need a clean slate::

        def teardown():
            clear_hooks(HOOK_POST_DOCUMENT_CREATE)
    """
    _REGISTRY[hook_name] = []


def get_handlers(hook_name: str) -> list[Callable]:
    """Return a copy of the handler list for *hook_name* (read-only)."""
    return list(_REGISTRY.get(hook_name, []))
