"""Generic registry primitive — the shared shape across the v0.3 + v0.4 registries.

See: ``docs/reference/cli.md`` §"The generic ``Registry[F]``
primitive" (consumers meet this in the same context as the CLI
subcommand registry, which is the canonical worked example).

v0.4 Phase G5. Five hand-rolled registries shipped in v0.3
(crash sinks, log handlers, redaction pattern groups, span
processors, vault backends) and one in v0.4 Phase E (CLI
subcommands). All six follow the same pattern:

  - a ``dict[str, Factory]`` of registered names
  - a ``register_X(name)`` decorator that raises on
    double-registration
  - an ``unregister_X(name)`` no-op-if-unknown
  - an ``_registered_X()`` snapshot for tests + diagnostic CLI

This module factors that pattern out into a single primitive.
**The existing six registries are NOT migrated** — they work and
migration carries risk for marginal reward. The primitive is the
shape for FUTURE registries: when v0.5+ adds another (the
v0.4-Phase-D-deferred ``register_vault_backend(config_model=...)``
extension is one candidate; consumer-built registries are
another), the primitive enforces uniformity.

Closed-box win: consumers building their OWN registries (e.g., a
``vmanager.providers.HYPERVISORS`` registry) get the same shape
``mgf.common`` uses internally. Pattern reuse lowers per-registry
cost when registries 7, 8, 9... land.

Usage::

    from collections.abc import Callable
    from typing import Any

    from mgf.common.registry import make_registry, Registry

    # Define the factory signature
    WidgetFactory = Callable[[dict[str, Any]], "Widget"]

    # Make the registry
    WIDGETS: Registry[WidgetFactory] = make_registry("widget")

    # Use the decorator + helpers exactly like the v0.3 registries
    @WIDGETS.register("local")
    def _make_local(config: dict[str, Any]) -> Widget:
        ...

    @WIDGETS.register("remote")
    def _make_remote(config: dict[str, Any]) -> Widget:
        ...

    # Look up + dispatch
    factory = WIDGETS.get("local")
    if factory is None:
        raise KeyError("unknown widget kind")
    widget = factory({"url": "..."})

    # Test introspection
    assert "local" in WIDGETS.snapshot()
    WIDGETS.unregister("local")  # no-op if unknown
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Generic, TypeVar

if TYPE_CHECKING:
    from collections.abc import Callable, Iterator

# Generic factory shape; the registry doesn't constrain it beyond
# "a callable thing".
F = TypeVar("F")


class Registry(Generic[F]):
    """Type-safe name → factory map.

    Created via :func:`make_registry`; the constructor is private
    in spirit (the recommended construction path is the helper).

    Threading: register at import / bootstrap time, not from a
    worker. The registry is read-only on the hot path.
    """

    def __init__(self, name: str) -> None:
        """Initialize an empty registry named ``name``.

        ``name`` is used in error messages (e.g. "vault backend
        'env' is already registered") + by the diagnostic CLI's
        future ``mgf-common doctor`` introspection.
        """
        self.name: str = name
        self._items: dict[str, F] = {}

    def register(self, key: str) -> Callable[[F], F]:
        """Decorator: register ``factory`` under ``key``.

        Raises :class:`ValueError` if ``key`` is already registered;
        callers MUST :meth:`unregister` before re-registering. This
        catches accidental double-registration in plug-in chains.

        Returns the original factory so chained decorators (e.g.
        ``@functools.cache`` outside ``@registry.register``) keep
        working.
        """

        def decorator(factory: F) -> F:
            if key in self._items:
                raise ValueError(
                    f"{self.name} {key!r} is already registered "
                    f"(unregister first if you intend to replace it)"
                )
            self._items[key] = factory
            return factory

        return decorator

    def unregister(self, key: str) -> None:
        """Remove ``key`` from the registry. No-op if unknown.

        Useful for tests + for consumers swapping a registered
        factory at runtime (the supported replacement pattern is
        :meth:`unregister` then :meth:`register`).
        """
        self._items.pop(key, None)

    def get(self, key: str) -> F | None:
        """Look up ``key``. Returns ``None`` if unknown.

        Callers SHOULD treat ``None`` as a typed exception trigger
        (e.g. ``raise VaultError(...)``) — the registry itself
        doesn't raise on misses to keep the lookup hot path
        branch-free.
        """
        return self._items.get(key)

    def snapshot(self) -> dict[str, F]:
        """Return a shallow copy of the current registry contents.

        The copy is safe to mutate; mutations DO NOT affect the
        live registry. Used by tests + by the (future)
        ``mgf-common doctor`` diagnostic that prints every
        registered factory.
        """
        return dict(self._items)

    def __iter__(self) -> Iterator[tuple[str, F]]:
        """Iterate ``(key, factory)`` pairs in insertion order.

        Insertion order is the natural shape for argparse-driven
        registries (e.g. CLI subcommands) where the help text
        wants to list things in registration order.
        """
        return iter(self._items.items())

    def __contains__(self, key: object) -> bool:
        """``"name" in registry`` — membership check."""
        return key in self._items

    def __len__(self) -> int:
        """Number of registered factories."""
        return len(self._items)

    def __repr__(self) -> str:
        return f"Registry({self.name!r}, {len(self._items)} items)"


def make_registry(name: str) -> Registry[F]:
    """Construct a :class:`Registry` named ``name``.

    The recommended construction path. ``name`` is used in error
    messages + by the diagnostic CLI's future introspection.

    Note on typing: the generic parameter ``F`` is inferred at the
    call site if you annotate the assignment::

        WIDGETS: Registry[WidgetFactory] = make_registry("widget")

    Without the explicit annotation, mypy infers ``Registry[F]``
    with an unbound ``F`` — works at runtime but loses type info
    on lookups.
    """
    return Registry(name)


__all__ = ["Registry", "make_registry"]
