"""Vocabulary registry for UKO Layer 2 paradigm vocabularies.

Provides a ``VocabularyRegistry`` that stores and retrieves
``ParadigmVocabulary`` instances by prefix or IRI.

Thread Safety
-------------
``VocabularyRegistry`` is **not** thread-safe.  External
synchronisation is required if instances are shared across threads
after initial population.  Typical usage creates and populates a
registry once during module initialisation under the GIL.
"""

from __future__ import annotations

from bisect import insort

import structlog

from cleveragents.acms.uko.vocabularies import ParadigmVocabulary

__all__: list[str] = ["DuplicateVocabularyError", "VocabularyRegistry"]

_log = structlog.get_logger(__name__)


class DuplicateVocabularyError(ValueError):
    """Raised when registering a vocabulary with a duplicate prefix or IRI."""


class VocabularyRegistry:
    """Registry of Layer 2 paradigm vocabularies.

    Provides lookup by prefix or IRI and lists all registered
    vocabularies.  An internal sorted prefix list is maintained so
    that ``list_prefixes`` and ``list_all`` avoid re-sorting on
    every call.

    Mutation Safety
    ~~~~~~~~~~~~~~~
    ``register()`` and ``unregister()`` mutate internal state.  The
    registry does **not** make defensive copies of vocabularies on
    retrieval; callers receive the original frozen
    ``ParadigmVocabulary`` instances.

    Args:
        vocabularies: Initial vocabularies to register.
    """

    def __init__(
        self,
        vocabularies: tuple[ParadigmVocabulary, ...] = (),
    ) -> None:
        self._by_prefix: dict[str, ParadigmVocabulary] = {}
        self._by_iri: dict[str, ParadigmVocabulary] = {}
        self._sorted_prefixes: list[str] = []
        for vocab in vocabularies:
            self.register(vocab)

    def register(self, vocabulary: ParadigmVocabulary) -> None:
        """Register a paradigm vocabulary.

        Args:
            vocabulary: The vocabulary to register.

        Raises:
            DuplicateVocabularyError: If a vocabulary with the same
                prefix or IRI is already registered.
        """
        if vocabulary.prefix in self._by_prefix:
            raise DuplicateVocabularyError(
                f"Vocabulary with prefix '{vocabulary.prefix}' already registered"
            )
        if vocabulary.iri in self._by_iri:
            raise DuplicateVocabularyError(
                f"Vocabulary with IRI '{vocabulary.iri}' already registered"
            )
        self._by_prefix[vocabulary.prefix] = vocabulary
        self._by_iri[vocabulary.iri] = vocabulary
        insort(self._sorted_prefixes, vocabulary.prefix)
        _log.debug(
            "vocabulary_registry.registered",
            prefix=vocabulary.prefix,
            iri=vocabulary.iri,
            num_classes=len(vocabulary.classes),
            num_properties=len(vocabulary.properties),
        )

    def unregister(self, prefix: str) -> bool:
        """Remove a vocabulary by prefix.

        Args:
            prefix: The prefix of the vocabulary to remove.

        Returns:
            ``True`` if the vocabulary was removed, ``False`` if the
            prefix was not registered.
        """
        vocab = self._by_prefix.pop(prefix, None)
        if vocab is None:
            return False
        self._by_iri.pop(vocab.iri, None)
        self._sorted_prefixes.remove(prefix)
        _log.debug("vocabulary_registry.unregistered", prefix=prefix)
        return True

    def get_by_prefix(self, prefix: str) -> ParadigmVocabulary | None:
        """Look up a vocabulary by its short prefix.

        Args:
            prefix: Short prefix (e.g., ``"uko-oo:"``).

        Returns:
            The matching vocabulary, or None if not found.
        """
        return self._by_prefix.get(prefix)

    def get_by_iri(self, iri: str) -> ParadigmVocabulary | None:
        """Look up a vocabulary by its full IRI.

        Args:
            iri: Full namespace IRI.

        Returns:
            The matching vocabulary, or None if not found.
        """
        return self._by_iri.get(iri)

    def list_prefixes(self) -> tuple[str, ...]:
        """Return all registered prefixes in sorted order."""
        return tuple(self._sorted_prefixes)

    def list_all(self) -> tuple[ParadigmVocabulary, ...]:
        """Return all registered vocabularies sorted by prefix."""
        return tuple(self._by_prefix[p] for p in self._sorted_prefixes)

    def __len__(self) -> int:
        """Return the number of registered vocabularies."""
        return len(self._by_prefix)

    def __contains__(self, item: object) -> bool:
        """Check if a prefix or IRI is registered."""
        if isinstance(item, str):
            return item in self._by_prefix or item in self._by_iri
        return False
