"""Thread-safe LSP server registry.

Provides :class:`LspRegistry`, a global registry of LSP server
configurations keyed by namespaced name.  The registry uses a
:class:`threading.RLock` to ensure safe concurrent access.

Typical usage::

    from cleveragents.lsp.registry import LspRegistry
    from cleveragents.lsp.models import LspServerConfig

    registry = LspRegistry()
    registry.register(LspServerConfig(
        name="local/pyright",
        command="pyright-langserver",
        languages=["python"],
    ))
    config = registry.get("local/pyright")
"""

from __future__ import annotations

import logging
import threading

from cleveragents.lsp.errors import LspServerNotFoundError
from cleveragents.lsp.models import LspServerConfig

logger = logging.getLogger(__name__)


class LspRegistry:
    """Thread-safe, in-memory registry for LSP server configurations.

    All public methods acquire an :class:`threading.RLock` before
    reading or mutating state, so concurrent calls from multiple
    threads are safe.

    Attributes:
        _servers: Internal mapping of namespaced name -> config.
        _lock: Reentrant lock guarding ``_servers``.
    """

    def __init__(self) -> None:
        self._servers: dict[str, LspServerConfig] = {}
        self._lock = threading.RLock()

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

    def register(self, config: LspServerConfig) -> None:
        """Register an LSP server configuration.

        Args:
            config: The :class:`LspServerConfig` to register.

        Raises:
            ValueError: If *config* is ``None`` or its name is already
                registered.
        """
        if config is None:
            raise ValueError("config must not be None")
        with self._lock:
            if config.name in self._servers:
                raise ValueError(f"LSP server '{config.name}' is already registered")
            self._servers[config.name] = config
            logger.info("Registered LSP server: %s", config.name)

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

    def get(self, name: str) -> LspServerConfig | None:
        """Look up an LSP server configuration by namespaced name.

        Args:
            name: Namespaced server name (e.g. ``local/pyright``).

        Returns:
            The :class:`LspServerConfig` if found, otherwise ``None``.

        Raises:
            ValueError: If *name* is empty.
        """
        if not name:
            raise ValueError("name must be a non-empty string")
        with self._lock:
            return self._servers.get(name)

    def get_or_raise(self, name: str) -> LspServerConfig:
        """Look up an LSP server or raise :class:`LspServerNotFoundError`.

        Args:
            name: Namespaced server name.

        Returns:
            The :class:`LspServerConfig`.

        Raises:
            LspServerNotFoundError: If the server is not registered.
        """
        config = self.get(name)
        if config is None:
            raise LspServerNotFoundError(name)
        return config

    # -- Listing ---------------------------------------------------------------

    def list_servers(
        self,
        namespace: str | None = None,
        language: str | None = None,
    ) -> list[LspServerConfig]:
        """List registered servers with optional filters.

        Args:
            namespace: If given, only return servers whose namespace
                matches (prefix before the last ``/``).
            language: If given, only return servers that support this
                language (case-insensitive).

        Returns:
            List of matching :class:`LspServerConfig` instances,
            sorted by name.
        """
        with self._lock:
            results: list[LspServerConfig] = []
            lang_lower = language.lower() if language else None
            for config in self._servers.values():
                if namespace is not None and config.namespace != namespace:
                    continue
                if lang_lower is not None and lang_lower not in config.languages:
                    continue
                results.append(config)
            results.sort(key=lambda c: c.name)
            return results

    # -- Removal ---------------------------------------------------------------

    def remove(self, name: str) -> bool:
        """Unregister a server by namespaced name.

        Args:
            name: The namespaced server name to remove.

        Returns:
            ``True`` if the server was removed, ``False`` if it was
            not registered.

        Raises:
            ValueError: If *name* is empty.
        """
        if not name:
            raise ValueError("name must be a non-empty string")
        with self._lock:
            if name in self._servers:
                del self._servers[name]
                logger.info("Removed LSP server: %s", name)
                return True
            return False

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

    def __len__(self) -> int:
        """Return the number of registered servers."""
        with self._lock:
            return len(self._servers)

    def __contains__(self, name: str) -> bool:
        """Check whether a server name is registered."""
        with self._lock:
            return name in self._servers


__all__ = [
    "LspRegistry",
]
