# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
# See LICENSE for details.
"""Generic registry for plugin catalog entries.

Defines the base Registry class for managing plugin descriptors with
attribute-based filtering via vcti-lookup.
"""

from collections.abc import Iterable, Iterator
from contextlib import contextmanager

from vcti.lookup import Lookup, attributes_getter

from .descriptor import Descriptor


class RegistryError(RuntimeError):
    """Base exception for registry errors."""


class DuplicateEntryError(RegistryError):
    """Raised when attempting to register a duplicate ID."""


class EntryNotFoundError(RegistryError):
    """Raised when a requested entry is not found."""


class Registry[D: Descriptor]:
    """Generic registry for managing plugin descriptors.

    Manages a collection of plugin descriptors, providing registration,
    lookup, and filtering capabilities.

    Type Parameters:
        D: The descriptor type (must be a subclass of Descriptor).

    Example:
        >>> from vcti.plugincatalog import Descriptor, Registry
        >>>
        >>> class MyPlugin:
        ...     def process(self): ...
        >>>
        >>> registry = Registry()
        >>> registry.register(Descriptor(
        ...     id="my-plugin", name="My Plugin", instance=MyPlugin(),
        ...     attributes={"category": "processing"},
        ... ))
        >>> plugin = registry.get("my-plugin").instance
    """

    def __init__(self):
        """Initialize an empty registry."""
        self._descriptors: dict[str, D] = {}
        self._lookup: Lookup[D] | None = None
        self._dirty = True

    # ------------------------------------------------------------------
    # Registration
    # ------------------------------------------------------------------

    def register(self, descriptor: D) -> None:
        """Register a plugin descriptor.

        Args:
            descriptor: Descriptor to register.

        Raises:
            DuplicateEntryError: If a descriptor with the same ID already exists.

        Example:
            >>> registry.register(my_descriptor)
        """
        if descriptor.id in self._descriptors:
            raise DuplicateEntryError(
                f"Descriptor with id '{descriptor.id}' is already registered."
            )

        self._descriptors[descriptor.id] = descriptor
        self._dirty = True

    def register_many(self, descriptors: Iterable[D]) -> None:
        """Register multiple descriptors.

        Note:
            Registration is **not** atomic. If a descriptor raises
            ``DuplicateEntryError``, all preceding descriptors in the
            iterable will already be registered.

        Args:
            descriptors: Iterable of descriptors to register.

        Example:
            >>> registry.register_many([desc1, desc2, desc3])
        """
        for descriptor in descriptors:
            self.register(descriptor)

    def unregister_many(self, descriptor_ids: Iterable[str]) -> None:
        """Remove multiple descriptors from the registry.

        Note:
            Removal is **not** atomic. If an ID raises
            ``EntryNotFoundError``, all preceding IDs will already be
            unregistered.

        Args:
            descriptor_ids: Iterable of descriptor IDs to remove.

        Example:
            >>> registry.unregister_many(["plugin-a", "plugin-b"])
        """
        for descriptor_id in descriptor_ids:
            self.unregister(descriptor_id)

    def unregister(self, descriptor_id: str) -> None:
        """Remove a descriptor from the registry.

        Args:
            descriptor_id: ID of the descriptor to remove.

        Raises:
            EntryNotFoundError: If the descriptor doesn't exist.

        Example:
            >>> registry.unregister("plugin-id")
        """
        if descriptor_id not in self._descriptors:
            raise EntryNotFoundError(f"Descriptor '{descriptor_id}' not found.")

        del self._descriptors[descriptor_id]
        self._dirty = True

    # ------------------------------------------------------------------
    # Lookup
    # ------------------------------------------------------------------

    def get(self, descriptor_id: str) -> D:
        """Retrieve a descriptor by ID.

        Args:
            descriptor_id: Unique descriptor identifier.

        Returns:
            The descriptor with the given ID.

        Raises:
            EntryNotFoundError: If descriptor is not found.

        Example:
            >>> descriptor = registry.get("plugin-id")
            >>> plugin = descriptor.instance
        """
        if descriptor_id not in self._descriptors:
            raise EntryNotFoundError(f"Descriptor '{descriptor_id}' not found.")

        return self._descriptors[descriptor_id]

    def find(self, descriptor_id: str) -> D | None:
        """Retrieve a descriptor by ID, returning ``None`` if not found.

        Unlike :meth:`get`, this method never raises an exception.

        Args:
            descriptor_id: Unique descriptor identifier.

        Returns:
            The descriptor, or ``None`` if no entry matches.

        Example:
            >>> desc = registry.find("maybe-missing")
            >>> if desc is not None:
            ...     print(desc.name)
        """
        return self._descriptors.get(descriptor_id)

    def all(self) -> list[D]:
        """Return all registered descriptors.

        Returns:
            List of all descriptors.

        Example:
            >>> for descriptor in registry.all():
            ...     print(descriptor.id, descriptor.name)
        """
        return list(self._descriptors.values())

    def ids(self) -> list[str]:
        """Return all registered descriptor IDs.

        Returns:
            List of descriptor IDs.

        Example:
            >>> print(registry.ids())
            ['plugin-1', 'plugin-2']
        """
        return list(self._descriptors.keys())

    # ------------------------------------------------------------------
    # Filtering
    # ------------------------------------------------------------------

    @property
    def lookup(self) -> Lookup[D]:
        """Get a Lookup for querying the registered descriptors.

        The lookup is cached and automatically rebuilt when descriptors change.
        Filters against descriptor `.attributes` dict values.

        Returns:
            Lookup instance for attribute-based filtering.

        Example:
            >>> from vcti.lookup import Rule
            >>> rules = [Rule("file_type", "==", "binary")]
            >>> matches = registry.lookup.filter(rules)
        """
        if self._dirty or self._lookup is None:
            self._lookup = Lookup(self.all(), getter=attributes_getter)
            self._dirty = False
        return self._lookup

    def create_lookup(self) -> Lookup[D]:
        """Create a new Lookup instance.

        Useful if you need independent lookup instances.

        Returns:
            New Lookup instance.

        Example:
            >>> lk = registry.create_lookup()
            >>> results = lk.filter(rules)
        """
        return Lookup(self.all(), getter=attributes_getter)

    # ------------------------------------------------------------------
    # Introspection
    # ------------------------------------------------------------------

    def __contains__(self, descriptor_id: str) -> bool:
        """Check if a descriptor is registered.

        Args:
            descriptor_id: Descriptor ID to check.

        Returns:
            True if descriptor exists, False otherwise.

        Example:
            >>> if "plugin-id" in registry:
            ...     descriptor = registry.get("plugin-id")
        """
        return descriptor_id in self._descriptors

    def __iter__(self) -> Iterator[D]:
        """Iterate over all registered descriptors.

        Returns:
            Iterator over descriptors in registration order.

        Example:
            >>> for descriptor in registry:
            ...     print(descriptor.id)
        """
        return iter(self._descriptors.values())

    def __len__(self) -> int:
        """Get number of registered descriptors.

        Returns:
            Count of registered descriptors.

        Example:
            >>> print(f"Registry has {len(registry)} plugins")
        """
        return len(self._descriptors)

    def __repr__(self) -> str:
        ids = list(self._descriptors.keys())
        if len(ids) <= 5:
            return f"<Registry entries={len(self)} ids={ids}>"
        shown = ", ".join(repr(i) for i in ids[:2])
        return f"<Registry entries={len(self)} ids=[{shown}, ...]>"

    # ------------------------------------------------------------------
    # Isolation for Testing
    # ------------------------------------------------------------------

    @classmethod
    @contextmanager
    def isolated(cls, preload: Iterable[D] | None = None) -> Iterator["Registry[D]"]:
        """Create an isolated temporary registry for testing.

        This context manager creates a new registry instance that is completely
        independent. Useful for unit tests to avoid state pollution.

        Args:
            preload: Optional iterable of descriptors to pre-register.

        Yields:
            A new isolated registry instance.

        Example:
            >>> with Registry.isolated() as registry:
            ...     registry.register(test_descriptor)
            ...     assert len(registry) == 1
            >>> # Registry is discarded after context exits
        """
        temp_registry = cls()
        if preload:
            temp_registry.register_many(preload)
        yield temp_registry

    def clear(self) -> None:
        """Remove all registered descriptors from the registry.

        Warning: This clears the registry completely. Use with caution.
        Prefer using isolated() for testing.

        Example:
            >>> registry.clear()
            >>> assert len(registry) == 0
        """
        self._descriptors.clear()
        self._dirty = True

    def copy(self) -> "Registry[D]":
        """Create a shallow copy of this registry.

        Returns:
            A new registry with the same descriptors.

        Note:
            The descriptors themselves are not deep-copied, only the registry structure.

        Example:
            >>> registry2 = registry.copy()
            >>> assert len(registry2) == len(registry)
        """
        new_registry = Registry[D]()
        new_registry._descriptors = self._descriptors.copy()
        new_registry._dirty = True
        return new_registry
