"""Generic Registry - Universal registration system for Pytola.

This module provides a flexible, type-safe registry system for managing
classes and components. It supports multiple independent registries and
does not depend on any specific module.

Usage Examples:
    # Create a module-specific registry
    my_registry = Registry("my_module")

    # Register a class
    my_registry.register("build", BuildClass, aliases=["b"])

    # Or use the decorator
    @register("my_module", name="build", aliases=["b"])
    class BuildClass:
        pass

    # Get registered class
    cls = my_registry.get("build")
"""

from __future__ import annotations

import threading
from typing import Any, Callable, ClassVar, Generic, TypeVar

from ..logger import logger

__all__ = ["DefaultRegistry", "Registry", "get_registry", "register"]

T = TypeVar("T", bound=type)


MAX_REGISTRY_NAME_LENGTH = 64


def _validate_registration_name(name: str) -> None:
    """Validate a registration name.

    Args:
        name: The name to validate

    Raises
    ------
        ValueError: If name is empty or exceeds maximum length
    """
    if not name:
        msg = "Registration name cannot be empty"
        raise ValueError(msg)
    if len(name) > MAX_REGISTRY_NAME_LENGTH:
        msg = f"Registration name '{name}' exceeds maximum length of {MAX_REGISTRY_NAME_LENGTH}"
        raise ValueError(msg)


def _validate_aliases(aliases: list[str]) -> None:
    """Validate a list of aliases.

    Args:
        aliases: List of aliases to validate

    Raises
    ------
        ValueError: If any alias is empty or exceeds maximum length
    """
    for alias in aliases:
        if not alias:
            msg = "Alias cannot be empty string"
            raise ValueError(msg)
        if len(alias) > MAX_REGISTRY_NAME_LENGTH:
            msg = f"Alias '{alias}' exceeds maximum length of {MAX_REGISTRY_NAME_LENGTH}"
            raise ValueError(msg)


class Registry(Generic[T]):
    """Generic registry for class registration.

    Each registry instance is independent and identified by a unique name.
    This allows different modules to have their own isolated registries
    while sharing the same underlying implementation.

    Attributes
    ----------
        name: Unique identifier for this registry

    Example:
        # Create registries for different modules
        command_registry = Registry[BaseCommand]("commands")
        plugin_registry = Registry[PluginBase]("plugins")

        # Register items in each
        command_registry.register("build", BuildCommand)
        plugin_registry.register("logger", LoggerPlugin)
    """

    _registries: ClassVar[dict[str, Registry]] = {}
    _lock: ClassVar[threading.Lock] = threading.Lock()

    def __new__(cls, name: str) -> Registry[T]:
        """Get or create a named registry instance.

        Args:
            name: Unique identifier for the registry

        Returns
        -------
            The registry instance (creates new if doesn't exist)
        """
        with cls._lock:
            if name not in cls._registries:
                instance = super().__new__(cls)
                instance._name = name
                instance._initialized = False

                cls._registries[name] = instance
            return cls._registries[name]

    def __init__(self, name: str) -> None:
        """Initialize the registry if not already initialized."""
        # Note: _initialized is set in __new__ before returning instance
        # so we can safely check it here. No lock needed here because:
        # 1. __new__ holds the lock when creating the instance
        # 2. __new__ sets _initialized=False before returning
        # 3. This method is idempotent - safe to call multiple times
        if getattr(self, "_initialized", False):
            return

        # At this point, we know _initialized is False
        # We need to acquire the lock to prevent race conditions
        with self._lock:
            # Double-check pattern: another thread may have initialized
            # while we were waiting for the lock or doing other work
            if getattr(self, "_initialized", False):
                return

            self._name = name
            self._items: dict[str, type[T]] = {}
            self._aliases: dict[str, str] = {}
            self._metadata: dict[str, dict[str, Any]] = {}
            self._initialized = True
            logger.debug(f"Registry '{name}' initialized")

    @property
    def name(self) -> str:
        """Get the registry name."""
        return self._name

    def register(
        self,
        name: str,
        item_class: type[T],
        aliases: list[str] | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> None:
        """Register a class.

        Args:
            name: Canonical name for the item
            item_class: The class to register
            aliases: Optional list of short names/aliases
            metadata: Optional metadata dictionary

        Raises
        ------
            ValueError: If name exceeds maximum length
        """
        # Validate inputs
        _validate_registration_name(name)
        if aliases:
            _validate_aliases(aliases)

        # Check for existing registration
        if name in self._items:
            logger.warning(f"Item '{name}' is already registered in '{self._name}'. Overwriting.")

        # Register the item
        self._items[name] = item_class
        self._metadata[name] = metadata or {}

        # Register aliases
        if aliases:
            self._register_aliases(aliases, name)

        logger.debug(f"Registered item '{name}' in '{self._name}'")

    def _register_aliases(self, aliases: list[str], name: str) -> None:
        """Register aliases for an item.

        Args:
            aliases: List of aliases to register
            name: The canonical name these aliases point to
        """
        for alias in aliases:
            if alias in self._aliases:
                logger.warning(f"Alias '{alias}' is already registered in '{self._name}'. Overwriting.")
            self._aliases[alias] = name
            logger.debug(f"Registered alias '{alias}' -> '{name}' in '{self._name}'")

    def unregister(self, name: str) -> None:
        """Unregister an item by name.

        Args:
            name: The canonical name of the item to remove
        """
        if name not in self._items:
            logger.warning(f"Attempted to unregister non-existent item '{name}' in '{self._name}'")
            return

        del self._items[name]

        # Remove associated aliases - optimized single pass
        for alias in list(self._aliases.keys()):
            if self._aliases[alias] == name:
                del self._aliases[alias]

        if name in self._metadata:
            del self._metadata[name]

        logger.info(f"Unregistered item '{name}' from '{self._name}'")

    def get(self, name_or_alias: str) -> type[T] | None:
        """Get a registered class by name or alias.

        Args:
            name_or_alias: The canonical name or alias

        Returns
        -------
            The registered class, or None if not found
        """
        canonical_name = self._aliases.get(name_or_alias, name_or_alias)
        item_class = self._items.get(canonical_name)
        if item_class is None:
            logger.debug(f"Item '{name_or_alias}' not found in registry '{self._name}'")
        return item_class

    def get_instance(self, name_or_alias: str, *args: object, **kwargs: object) -> T | None:
        """Get an instance of a registered class.

        Args:
            name_or_alias: The canonical name or alias
            *args: Positional arguments for instantiation
            **kwargs: Keyword arguments for instantiation

        Returns
        -------
            An instance of the class, or None if not found
        """
        item_class = self.get(name_or_alias)
        if item_class is None:
            return None
        return item_class(*args, **kwargs)

    def get_classes(self) -> list[type[T]]:
        """Get a list of all registered classes.

        Returns
        -------
            List of registered classes
        """
        return list(self._items.values())

    def get_metadata(self, name: str) -> dict[str, Any]:
        """Get metadata for a registered item.

        Args:
            name: The canonical name

        Returns
        -------
            Metadata dictionary (empty if not found).
            Includes both standard fields (name, class_name, description, etc.)
            and any custom metadata fields provided during registration.
        """
        return self._metadata.get(name, {}).copy()

    def list_items(self) -> list[str]:
        """List all registered item names.

        Returns
        -------
            List of canonical names
        """
        return list(self._items.keys())

    def list_all(self) -> dict[str, Any]:
        """List all registered items with metadata.

        Returns
        -------
            Dictionary mapping names to metadata dicts.
            Note: The returned dicts include a 'class' key with the actual class object.
        """
        result = {}
        for name, item_class in self._items.items():
            metadata = self._metadata.get(name, {}).copy()
            metadata["class"] = item_class
            metadata["aliases"] = [alias for alias, target in self._aliases.items() if target == name]
            result[name] = metadata
        return result

    def clear(self) -> None:
        """Clear all registered items from this registry."""
        self._items.clear()
        self._aliases.clear()
        self._metadata.clear()
        logger.debug(f"Registry '{self._name}' cleared")

    def __len__(self) -> int:
        """Return the number of registered items.

        Returns
        -------
            int: The count of registered items.
        """
        return len(self._items)

    def __contains__(self, name: str) -> bool:
        """Check if an item is registered.

        Args:
            name (str): The item name to check.

        Returns
        -------
            bool: True if item is registered, False otherwise.
        """
        canonical_name = self._aliases.get(name, name)
        return canonical_name in self._items

    @classmethod
    def get_registry(cls, name: str) -> Registry[Any]:
        """Get a registry by name (convenience method).

        Args:
            name: Registry identifier

        Returns
        -------
            The registry instance
        """
        return cls(name)

    @classmethod
    def list_registries(cls) -> list[str]:
        """List all registered registry names.

        Returns
        -------
            List of registry names
        """
        return list(cls._registries.keys())

    @classmethod
    def clear_all(cls) -> None:
        """Clear all registries (useful for testing)."""
        with cls._lock:
            for registry in cls._registries.values():
                registry.clear()
            cls._registries.clear()
            logger.debug("All registries cleared")


class DefaultRegistry(Registry[Any]):
    """Backward-compatible generic registry.

    This class provides backward compatibility for code using the old
    singleton-based GenericRegistry. It wraps Registry("default") to
    ensure all legacy code shares the same registry instance.

    Note: Use Registry[T] directly for new code.
    """

    def __new__(cls) -> Registry:
        """Return the default registry instance."""
        # Fast path - check without lock first
        if "default" in Registry._registries:
            return Registry._registries["default"]

        # Slow path - acquire lock and create if needed
        with cls._lock:
            # Double-check after acquiring lock
            if "default" not in Registry._registries:
                # Create directly by calling parent's __new__ to avoid lock contention
                instance: Registry[Any] = object.__new__(Registry)
                instance._name = "default"
                instance._initialized = False
                instance._items = {}
                instance._aliases = {}
                instance._metadata = {}
                instance._initialized = True
                Registry._registries["default"] = instance

        return Registry._registries["default"]

    def __init__(self) -> None:
        """No-op init - the registry is managed by __new__."""

    @classmethod
    def get_default_registry(cls) -> Registry:
        """Get the default registry instance (backward compatibility).

        Returns
        -------
            Registry: The default registry instance.
        """
        return cls()


def register(
    registry_or_name: str | None = None,
    *,
    name: str | None = None,
    aliases: list[str] | None = None,
    metadata: dict[str, Any] | None = None,
    description: str = "",
    help_text: str = "",
    **meta_kwargs: Any,  # noqa: ANN401
) -> Callable[[type[T]], type[T]]:
    """Register a class with a registry.

    This decorator supports two calling conventions:

    1. New style (recommended): Specify registry name as first argument
        @register("commands", name="build", aliases=["b"])
        class BuildCommand: ...

    2. Legacy style (backward compatible): Use default registry
        @register(name="build", aliases=["b"], description="...")
        class BuildCommand: ...

    Args:
        registry_or_name: Name of the registry to use (new style),
                         or None for default registry (legacy style)
        name: Canonical name (defaults to class name in lowercase)
        aliases: Optional list of aliases
        metadata: Optional metadata dictionary
        description: Legacy parameter for description
        help_text: Legacy parameter for help text
        **meta_kwargs: Additional metadata as keyword arguments

    Returns
    -------
        Decorator function
    """

    def decorator(cls: type[T]) -> type[T]:
        # Determine registry name and item name
        reg_name = registry_or_name
        item_name = name

        # Handle legacy case where decorator was called without registry name
        # but with keyword arguments like @register(name="...", aliases=[...])
        if reg_name is None:
            reg_name = "default"
        elif not isinstance(reg_name, str) or " " in reg_name:
            # First arg is not a valid registry name, treat it as item name (legacy)
            item_name = reg_name
            reg_name = "default"

        final_item_name = item_name or cls.__name__.lower()

        reg = Registry(reg_name)

        # Merge metadata - support both new style and legacy style
        final_metadata = (metadata or {}).copy()
        final_metadata.update(meta_kwargs)
        final_metadata.setdefault("name", final_item_name)
        final_metadata.setdefault("class_name", cls.__name__)

        # Support legacy description/help_text parameters
        if description:
            final_metadata.setdefault("description", description)
        if help_text:
            final_metadata.setdefault("help_text", help_text)

        reg.register(name=final_item_name, item_class=cls, aliases=aliases, metadata=final_metadata)

        # Store metadata on class for backward compatibility
        cls._registry_metadata = final_metadata  # type: ignore[attr-defined]
        cls._registry_name = reg_name  # type: ignore[attr-defined]
        cls._action_metadata = final_metadata  # type: ignore[attr-defined]  # Legacy compatibility

        logger.debug(f"Decorated and registered '{final_item_name}' in '{reg_name}'")
        return cls

    return decorator


def get_registry(name: str) -> Registry[Any]:
    """Get a registry by name.

    Args:
        name: Registry identifier

    Returns
    -------
        The registry instance

    Example:
        commands = get_registry("commands")
        build_class = commands.get("build")
    """
    return Registry(name)
