"""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 logging
from typing import Any, Callable, ClassVar, Dict, Generic, List, Optional, Type, TypeVar

from typing_extensions import Self

T = TypeVar("T")

logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)

MAX_REGISTRY_NAME_LENGTH = 64


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]] = {}

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

        Args:
            name: Unique identifier for the registry

        Returns
        -------
            The registry instance (creates new if doesn't exist)
        """
        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."""
        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] = {}
        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: Optional[List[str]] = None,
        metadata: Optional[dict] = 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 is already registered
        """
        if name in self._items:
            logger.warning(f"Item '{name}' is already registered in '{self._name}'. Overwriting.")

        self._items[name] = item_class
        self._metadata[name] = metadata or {}

        if aliases:
            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}'")

        logger.debug(f"Registered item '{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
        aliases_to_remove = [alias for alias, target in self._aliases.items() if target == name]
        for alias in aliases_to_remove:
            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) -> Optional[Type[T]]:
        """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) -> Optional[T]:
        """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:
        """Get metadata for a registered item.

        Args:
            name: The canonical name

        Returns
        -------
            Metadata dictionary (empty if not found)
        """
        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, dict]:
        """List all registered items with metadata.

        Returns
        -------
            Dictionary mapping names to metadata dicts
        """
        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:
        """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)."""
        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) -> Self:
        """Return the default registry instance."""
        # Ensure default registry exists and return it
        if "default" not in Registry._registries:  # noqa: SLF001
            Registry("default")
        # Return the default registry (type cast for compatibility)
        return Registry._registries["default"]  # type: ignore[return-value]  # noqa: SLF001

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

    @classmethod
    def get_instance(cls) -> Self:
        """Get the singleton instance (backward compatibility).

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


def register(
    registry_or_name: Optional[str] = None,
    *,
    name: Optional[str] = None,
    aliases: Optional[List[str]] = None,
    metadata: Optional[dict] = None,
    description: str = "",
    help_text: str = "",
    **meta_kwargs: object,
) -> 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
        # If first arg looks like a registry name (no spaces, reasonable length), use it
        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 " " in reg_name or len(reg_name) > MAX_REGISTRY_NAME_LENGTH:
            # First arg is not a 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
        cls._registry_name = reg_name
        cls._action_metadata = final_metadata  # Legacy compatibility

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

    return decorator


def get_registry(name: str) -> Registry:
    """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)
