"""π* registry: ``name@version`` → :class:`PiStar` instance.

Mutation discipline: registry is populated at module-import time via
:func:`register`. Tests that need to modify the registry should
import + register their fakes before any cache_key derivation runs.

There is no public ``unregister`` — once a name@version exists in
the runtime, every cache_key that hashed it stays valid against it.
Removing a key is equivalent to changing the cache_key invariant
silently; we forbid it.
"""

from __future__ import annotations

from typing import Iterator

from arborist.pi_star.protocol import PiStar, PiStarError, registry_key


REGISTRY: dict[str, PiStar] = {}


def register(pi_star: PiStar) -> None:
    """Insert a π* into the registry. Re-registration of the same key
    with a different instance is rejected — keys are content-pinned.
    """
    key = registry_key(pi_star)
    if key in REGISTRY and REGISTRY[key] is not pi_star:
        raise PiStarError(
            f"π* registry key already bound: {key} "
            f"(existing={type(REGISTRY[key]).__name__}, "
            f"incoming={type(pi_star).__name__})"
        )
    REGISTRY[key] = pi_star


def get(key: str) -> PiStar:
    """Look up a π* by ``name@version`` key. Raises ``KeyError`` if missing."""
    if key not in REGISTRY:
        raise KeyError(f"unknown π*: {key}")
    return REGISTRY[key]


def list_keys() -> list[str]:
    """All registered ``name@version`` keys, sorted."""
    return sorted(REGISTRY)


def domains() -> dict[str, list[str]]:
    """Map domain → sorted list of registered keys for that domain."""
    out: dict[str, list[str]] = {}
    for key, ps in REGISTRY.items():
        out.setdefault(ps.domain, []).append(key)
    for d in out:
        out[d] = sorted(out[d])
    return out


def __iter__() -> Iterator[str]:  # pragma: no cover — convenience
    return iter(REGISTRY)
