"""Provider Registry for AI Model Provider Discovery and Selection.

This module implements the provider registry that discovers configured providers
from Settings, reports capability metadata, and selects defaults based on
CLEVERAGENTS_DEFAULT_PROVIDER / CLEVERAGENTS_DEFAULT_MODEL environment variables.

Following ADR-008 (Provider Plugin Architecture), this registry provides:
- Dynamic provider discovery based on configured API keys
- Capability metadata for feature support (streaming, tool calls, etc.)
- Default selection based on configuration
- Fallback chains with graceful degradation
"""

from __future__ import annotations

import os
import threading
from dataclasses import dataclass
from enum import StrEnum
from typing import TYPE_CHECKING, Any, ClassVar

import structlog
from pydantic import BaseModel, ConfigDict, Field

from cleveragents.config.settings import Settings, get_settings
from cleveragents.domain.providers.ai_provider import AIProviderInterface

if TYPE_CHECKING:
    from langchain_core.language_models import BaseLanguageModel


logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)


def _coerce_optional_str(value: object | None) -> str | None:
    if value is None:
        return None
    if isinstance(value, str):
        return value
    return str(value)


def _coerce_env_bool(value: str) -> bool:
    """Consistent boolean coercion for environment variables.

    Accepts ``true``, ``1``, ``yes``, and ``on`` (case-insensitive) as truthy.
    """
    return value.strip().lower() in ("true", "1", "yes", "on")


class _ApiKeyMissing:
    """Sentinel distinguishing "api_key was not provided" from ``None``."""


_API_KEY_MISSING = _ApiKeyMissing()


class ProviderType(StrEnum):
    """Supported AI provider types."""

    OPENAI = "openai"
    ANTHROPIC = "anthropic"
    GOOGLE = "google"
    AZURE = "azure"
    OPENROUTER = "openrouter"
    GEMINI = "gemini"
    COHERE = "cohere"
    GROQ = "groq"
    TOGETHER = "together"
    MOCK = "mock"


@dataclass(frozen=True)
class ProviderCapabilities:
    """Capability metadata for a provider.

    Attributes:
        supports_streaming: Whether the provider supports streaming responses
        supports_tool_calls: Whether the provider supports tool/function calling
        supports_vision: Whether the provider supports image inputs
        max_context_length: Maximum context length in tokens
        supports_json_mode: Whether the provider supports structured JSON output
    """

    supports_streaming: bool = True
    supports_tool_calls: bool = False
    supports_vision: bool = False
    max_context_length: int = 4096
    supports_json_mode: bool = False


class ProviderInfo(BaseModel):
    """Information about a registered provider."""

    provider_type: ProviderType
    name: str
    api_key_env_var: str
    default_model: str
    capabilities: ProviderCapabilities = Field(default_factory=ProviderCapabilities)
    is_configured: bool = False

    model_config = ConfigDict(str_strip_whitespace=True)


class ProviderRegistry:
    """Registry for discovering and managing AI providers.

    The registry discovers configured providers based on environment variables
    and provides methods for selecting providers and models.

    Example:
        registry = ProviderRegistry()
        provider = registry.get_default_provider()
        if provider:
            response = provider.generate_changes(project, plan, contexts)
    """

    # Default provider capabilities for each provider type
    DEFAULT_CAPABILITIES: ClassVar[dict[ProviderType, ProviderCapabilities]] = {
        ProviderType.OPENAI: ProviderCapabilities(
            supports_streaming=True,
            supports_tool_calls=True,
            supports_vision=True,
            max_context_length=128000,
            supports_json_mode=True,
        ),
        ProviderType.ANTHROPIC: ProviderCapabilities(
            supports_streaming=True,
            supports_tool_calls=True,
            supports_vision=True,
            max_context_length=200000,
            supports_json_mode=False,
        ),
        ProviderType.GOOGLE: ProviderCapabilities(
            supports_streaming=True,
            supports_tool_calls=True,
            supports_vision=True,
            max_context_length=1000000,
            supports_json_mode=True,
        ),
        ProviderType.GEMINI: ProviderCapabilities(
            supports_streaming=True,
            supports_tool_calls=True,
            supports_vision=True,
            max_context_length=1000000,
            supports_json_mode=True,
        ),
        ProviderType.AZURE: ProviderCapabilities(
            supports_streaming=True,
            supports_tool_calls=True,
            supports_vision=True,
            max_context_length=128000,
            supports_json_mode=True,
        ),
        ProviderType.OPENROUTER: ProviderCapabilities(
            supports_streaming=True,
            supports_tool_calls=True,
            supports_vision=True,
            max_context_length=128000,
            supports_json_mode=True,
        ),
        ProviderType.COHERE: ProviderCapabilities(
            supports_streaming=True,
            supports_tool_calls=True,
            supports_vision=False,
            max_context_length=128000,
            supports_json_mode=False,
        ),
        ProviderType.GROQ: ProviderCapabilities(
            supports_streaming=True,
            supports_tool_calls=True,
            supports_vision=False,
            max_context_length=32000,
            supports_json_mode=True,
        ),
        ProviderType.TOGETHER: ProviderCapabilities(
            supports_streaming=True,
            supports_tool_calls=True,
            supports_vision=False,
            max_context_length=32000,
            supports_json_mode=False,
        ),
        ProviderType.MOCK: ProviderCapabilities(
            supports_streaming=False,
            supports_tool_calls=False,
            supports_vision=False,
            max_context_length=4096,
            supports_json_mode=False,
        ),
    }

    # Default models for each provider type
    DEFAULT_MODELS: ClassVar[dict[ProviderType, str]] = {
        ProviderType.OPENAI: "gpt-4o",
        ProviderType.ANTHROPIC: "claude-sonnet-4-20250514",
        ProviderType.GOOGLE: "gemini-2.0-flash",
        ProviderType.GEMINI: "gemini-2.0-flash",
        ProviderType.AZURE: "gpt-4o",
        ProviderType.OPENROUTER: "anthropic/claude-sonnet-4-20250514",
        ProviderType.COHERE: "command-r-plus",
        ProviderType.GROQ: "llama-3.1-70b-versatile",
        ProviderType.TOGETHER: "meta-llama/Llama-3.1-70B-Instruct-Turbo",
        ProviderType.MOCK: "mock-gpt",
    }

    # Mapping of provider type to Settings attribute name for API key
    PROVIDER_KEY_ATTRS: ClassVar[dict[ProviderType, str]] = {
        ProviderType.OPENAI: "openai_api_key",
        ProviderType.ANTHROPIC: "anthropic_api_key",
        ProviderType.GOOGLE: "google_api_key",
        ProviderType.GEMINI: "gemini_api_key",
        ProviderType.AZURE: "azure_api_key",
        ProviderType.OPENROUTER: "openrouter_api_key",
        ProviderType.COHERE: "cohere_api_key",
        ProviderType.GROQ: "groq_api_key",
        ProviderType.TOGETHER: "together_api_key",
    }

    # Provider priority order for fallback selection
    FALLBACK_ORDER: ClassVar[list[ProviderType]] = [
        ProviderType.OPENAI,
        ProviderType.ANTHROPIC,
        ProviderType.GOOGLE,
        ProviderType.AZURE,
        ProviderType.OPENROUTER,
        ProviderType.GROQ,
        ProviderType.TOGETHER,
        ProviderType.COHERE,
    ]

    def __init__(self, settings: Settings | None = None) -> None:
        """Initialize the provider registry.

        Args:
            settings: Optional settings instance. Uses global settings if not provided.
        """
        self._settings = settings or get_settings()
        self._providers: dict[ProviderType, ProviderInfo] = {}
        self._discover_providers()

    def _discover_providers(self) -> None:
        """Discover configured providers from settings."""
        for provider_type, key_attr in self.PROVIDER_KEY_ATTRS.items():
            api_key = getattr(self._settings, key_attr, None)
            is_configured = bool(api_key)

            provider_info = ProviderInfo(
                provider_type=provider_type,
                name=provider_type.value.title(),
                api_key_env_var=key_attr.upper(),
                default_model=self.DEFAULT_MODELS.get(provider_type, "unknown"),
                capabilities=self.DEFAULT_CAPABILITIES.get(
                    provider_type, ProviderCapabilities()
                ),
                is_configured=is_configured,
            )
            self._providers[provider_type] = provider_info

        # Register MOCK with conditional ``is_configured``.  The guard field
        # ``api_key_env_var`` points to ``CLEVERAGENTS_ALLOW_MOCK_PROVIDER``
        # semantically (it is the activation flag) even though it is not a
        # traditional API key.
        mock_allowed = _coerce_env_bool(
            os.environ.get("CLEVERAGENTS_ALLOW_MOCK_PROVIDER", "")
        )
        mock_info = ProviderInfo(
            provider_type=ProviderType.MOCK,
            name=ProviderType.MOCK.value.title(),
            api_key_env_var="CLEVERAGENTS_ALLOW_MOCK_PROVIDER",
            default_model=self.DEFAULT_MODELS.get(ProviderType.MOCK, "unknown"),
            capabilities=self.DEFAULT_CAPABILITIES.get(
                ProviderType.MOCK, ProviderCapabilities()
            ),
            is_configured=mock_allowed,
        )
        self._providers[ProviderType.MOCK] = mock_info

    def get_configured_providers(self) -> list[ProviderInfo]:
        """Get list of all configured providers.

        Returns:
            List of ProviderInfo for providers with valid credentials.
        """
        return [p for p in self._providers.values() if p.is_configured]

    def get_all_providers(self) -> list[ProviderInfo]:
        """Get list of all known providers (configured or not).

        Returns:
            List of all ProviderInfo instances.
        """
        return list(self._providers.values())

    def get_provider_info(
        self, provider_type: ProviderType | str
    ) -> ProviderInfo | None:
        """Get information about a specific provider.

        Args:
            provider_type: The provider type to look up.

        Returns:
            ProviderInfo if found, None otherwise.
        """
        if not isinstance(provider_type, ProviderType):
            try:
                provider_type = ProviderType(provider_type.lower())
            except ValueError:
                return None
        return self._providers.get(provider_type)

    def is_provider_configured(self, provider_type: ProviderType | str) -> bool:
        """Check if a provider is configured with valid credentials.

        Args:
            provider_type: The provider type to check.

        Returns:
            True if the provider has valid credentials.
        """
        info = self.get_provider_info(provider_type)
        return info.is_configured if info else False

    def get_default_provider_type(self) -> ProviderType | None:
        """Get the default provider type based on configuration.

        Order of precedence:
        1. CLEVERAGENTS_DEFAULT_PROVIDER environment variable
        2. First configured provider in fallback order

        Returns:
            The default provider type, or None if no provider is configured.
        """
        # Check environment variable first
        env_provider = os.environ.get("CLEVERAGENTS_DEFAULT_PROVIDER", "").lower()
        if env_provider:
            try:
                provider_type = ProviderType(env_provider)
                if self.is_provider_configured(provider_type):
                    logger.debug(
                        "provider_selected",
                        provider=provider_type.value,
                        reason="environment_variable",
                    )
                    return provider_type
                logger.debug(
                    "provider_skipped",
                    provider=env_provider,
                    reason="not_configured",
                )
            except ValueError:
                logger.debug(
                    "provider_skipped",
                    provider=env_provider,
                    reason="invalid_provider_type",
                )

        # Use settings default when configured
        settings_default = (self._settings.default_provider or "").lower()
        if settings_default:
            try:
                provider_type = ProviderType(settings_default)
                if self.is_provider_configured(provider_type):
                    logger.debug(
                        "provider_selected",
                        provider=provider_type.value,
                        reason="settings_default",
                    )
                    return provider_type
                logger.debug(
                    "provider_skipped",
                    provider=settings_default,
                    reason="not_configured",
                )
            except ValueError:
                logger.debug(
                    "provider_skipped",
                    provider=settings_default,
                    reason="invalid_provider_type",
                )

        # Fall back to first configured provider in priority order
        skipped: list[str] = []
        for provider_type in self.FALLBACK_ORDER:
            if self.is_provider_configured(provider_type):
                logger.debug(
                    "provider_selected",
                    provider=provider_type.value,
                    reason="fallback_order",
                    skipped_providers=skipped,
                )
                return provider_type
            skipped.append(provider_type.value)

        logger.debug(
            "no_provider_selected",
            reason="none_configured",
            skipped_providers=skipped,
        )
        return None

    def get_default_model(
        self, provider_type: ProviderType | str | None = None
    ) -> str | None:
        """Get the default model for a provider.

        Order of precedence:
        1. CLEVERAGENTS_DEFAULT_MODEL environment variable
        2. Provider's default model

        Args:
            provider_type: Optional provider type. Uses default if not specified.

        Returns:
            The default model ID, or None if no provider is configured.
        """
        # Check environment variable first
        env_model = os.environ.get("CLEVERAGENTS_DEFAULT_MODEL", "")
        if env_model:
            return env_model

        settings_model = self._settings.default_model
        if settings_model:
            return settings_model

        # Get provider type
        if provider_type is None:
            provider_type = self.get_default_provider_type()
            if provider_type is None:
                return None
        elif not isinstance(provider_type, ProviderType):
            try:
                provider_type = ProviderType(provider_type.lower())
            except ValueError:
                return None

        return self.DEFAULT_MODELS.get(provider_type)

    def _get_api_key(self, provider_type: ProviderType) -> str | None:
        """Get and validate the API key for a provider.

        Args:
            provider_type: The provider type to look up.

        Returns:
            The API key string, or ``None`` if the provider has no key
            requirement (e.g. ``MOCK`` when explicitly allowed).

        Raises:
            ValueError: If the provider requires an API key but it is not
                configured, or if ``MOCK`` is requested without the
                ``CLEVERAGENTS_ALLOW_MOCK_PROVIDER`` environment variable,
                or if the provider type has no API key mapping.
        """
        if provider_type == ProviderType.MOCK:
            if _coerce_env_bool(os.environ.get("CLEVERAGENTS_ALLOW_MOCK_PROVIDER", "")):
                return None
            raise ValueError(
                "Mock provider is not allowed in production. "
                "Set CLEVERAGENTS_ALLOW_MOCK_PROVIDER=true to enable it."
            )

        key_attr = self.PROVIDER_KEY_ATTRS.get(provider_type)
        if key_attr:
            api_key = getattr(self._settings, key_attr, None)
            if not api_key:
                raise ValueError(
                    f"Provider {provider_type.value} is not configured. "
                    f"Please set the {key_attr.upper()} environment variable."
                )
            return api_key

        # ProviderType is in the enum but not in PROVIDER_KEY_ATTRS
        raise ValueError(
            f"Provider {provider_type.value} has no API key mapping in "
            "PROVIDER_KEY_ATTRS."
        )

    def _create_provider_instance(
        self,
        provider_type: ProviderType,
        model_id: str | None,
        **kwargs: Any,
    ) -> BaseLanguageModel:
        """Single internal factory for provider instantiation.

        This is the single source of truth for raw LLM instantiation. Both
        ``create_llm()`` and ``create_ai_provider()`` delegate here so that
        adding a new provider requires touching exactly one place.

        Args:
            provider_type: The provider type.
            model_id: The model ID (provider default used when ``None``).
            **kwargs: Extra constructor arguments forwarded to the LLM.
                If ``api_key`` is present, it is consumed here instead of
                being fetched from settings. ``max_retries`` is consumed
                if present and never forwarded to LangChain constructors.

        Returns:
            A ``BaseLanguageModel`` instance for the requested provider.

        Raises:
            ValueError: If the provider is not configured or not supported.
        """
        # Consume retry metadata so it never leaks into provider constructors.
        kwargs.pop("max_retries", None)

        # If the caller explicitly passed api_key (even ``None``), use it
        # directly; otherwise fall back to _get_api_key.
        sentinel = kwargs.pop("__api_key_sentinel", _API_KEY_MISSING)
        if sentinel is _API_KEY_MISSING:
            api_key: str | None = self._get_api_key(provider_type)
        else:
            api_key = sentinel

        if provider_type == ProviderType.OPENAI:
            from langchain_openai import ChatOpenAI

            return ChatOpenAI(model=model_id or "gpt-4o", api_key=api_key, **kwargs)

        if provider_type == ProviderType.ANTHROPIC:
            from langchain_anthropic import ChatAnthropic

            return ChatAnthropic(
                model=model_id or "claude-sonnet-4-20250514",
                api_key=api_key,
                **kwargs,
            )

        if provider_type in (ProviderType.GOOGLE, ProviderType.GEMINI):
            from langchain_google_genai import ChatGoogleGenerativeAI

            return ChatGoogleGenerativeAI(
                model=model_id or "gemini-2.0-flash",
                google_api_key=api_key,
                **kwargs,
            )

        if provider_type == ProviderType.AZURE:
            from langchain_openai import AzureChatOpenAI

            deployment_override = _coerce_optional_str(
                kwargs.pop("deployment_name", None)
            )
            deployment_name = (
                deployment_override
                or model_id
                or self._settings.azure_openai_deployment
                or "gpt-4o"
            )

            endpoint_override = _coerce_optional_str(
                kwargs.pop("azure_endpoint", self._settings.azure_openai_endpoint)
            )
            azure_endpoint = endpoint_override or self._settings.azure_openai_endpoint
            if not azure_endpoint:
                raise ValueError(
                    "Azure OpenAI endpoint not configured. "
                    "Set AZURE_OPENAI_ENDPOINT or CLEVERAGENTS_AZURE_OPENAI_ENDPOINT."
                )

            api_version_override = _coerce_optional_str(
                kwargs.pop("api_version", self._settings.azure_openai_api_version)
            )
            api_version = api_version_override or "2024-05-01-preview"

            return AzureChatOpenAI(
                deployment_name=deployment_name,
                azure_endpoint=azure_endpoint,
                api_version=api_version,
                api_key=api_key,
                **kwargs,
            )

        if provider_type == ProviderType.GROQ:
            from langchain_groq import ChatGroq

            return ChatGroq(
                model=model_id or "llama-3.1-70b-versatile",
                api_key=api_key,
                **kwargs,
            )

        if provider_type == ProviderType.TOGETHER:
            from langchain_together import ChatTogether

            return ChatTogether(
                model=model_id or "meta-llama/Llama-3.1-70B-Instruct-Turbo",
                api_key=api_key,
                **kwargs,
            )

        if provider_type == ProviderType.COHERE:
            from langchain_cohere import ChatCohere

            return ChatCohere(
                model=model_id or "command-r-plus",
                api_key=api_key,
                **kwargs,
            )

        if provider_type == ProviderType.OPENROUTER:
            from langchain_openai import ChatOpenAI

            raw_headers = kwargs.pop("default_headers", None)
            sanitized_headers: dict[str, str] | None = (
                {str(k): str(v) for k, v in raw_headers.items()}
                if raw_headers
                else None
            )
            org_value = self._settings.openrouter_organization
            if org_value:
                org_value = org_value.strip()
                if org_value:
                    if sanitized_headers is None:
                        sanitized_headers = {}
                    sanitized_headers.setdefault("HTTP-Referer", org_value)
                    sanitized_headers.setdefault("X-Title", org_value)
            openrouter_kwargs: dict[str, Any] = {
                "model": model_id or "anthropic/claude-sonnet-4-20250514",
                "openai_api_base": "https://openrouter.ai/api/v1",
                "openai_api_key": api_key,
            }
            if sanitized_headers:
                openrouter_kwargs["default_headers"] = sanitized_headers
            openrouter_kwargs.update(kwargs)
            return ChatOpenAI(**openrouter_kwargs)

        if provider_type == ProviderType.MOCK:
            from langchain_community.llms import FakeListLLM

            return FakeListLLM(responses=["mock response"] * 10)

        raise ValueError(f"Unsupported provider type: {provider_type}")

    def _create_provider_llm(
        self,
        provider_type: ProviderType,
        model_id: str | None,
        **kwargs: Any,
    ) -> BaseLanguageModel:
        """Create a raw LLM instance for a provider.

        Thin wrapper around ``_create_provider_instance``.
        """
        return self._create_provider_instance(provider_type, model_id, **kwargs)

    def _resolve_provider_type(
        self,
        provider_type: ProviderType | str | None = None,
    ) -> ProviderType:
        """Resolve a possibly ``None`` or string provider type to ``ProviderType``.

        Uses the configured fallback chain when ``None`` and performs
        case-insensitive string matching otherwise.

        Raises:
            ValueError: If no provider is configured or the name is unknown.
        """
        if provider_type is None:
            resolved = self.get_default_provider_type()
            if resolved is None:
                raise ValueError(
                    "No AI provider configured. Please set one of the following "
                    "environment variables: OPENAI_API_KEY, ANTHROPIC_API_KEY, "
                    "GOOGLE_API_KEY, or another supported provider key."
                )
            return resolved
        if not isinstance(provider_type, ProviderType):
            try:
                return ProviderType(provider_type.lower())
            except ValueError as e:
                raise ValueError(f"Unknown provider type: {provider_type}") from e
        return provider_type

    def create_llm(
        self,
        provider_type: ProviderType | str | None = None,
        model_id: str | None = None,
        **kwargs: Any,
    ) -> BaseLanguageModel:
        """Create a LangChain LLM instance for a provider.

        Args:
            provider_type: The provider type. Uses default if not specified.
            model_id: The model ID. Uses default for provider if not specified.
            **kwargs: Additional arguments passed to the LLM constructor.

        Returns:
            A LangChain BaseLanguageModel instance.

        Raises:
            ValueError: If no provider is configured or provider is invalid.
        """
        resolved = self._resolve_provider_type(provider_type)

        if model_id is None:
            model_id = self.get_default_model(resolved)

        # Delegate to the unified factory (API key validation happens inside)
        return self._create_provider_instance(resolved, model_id, **kwargs)

    def create_ai_provider(
        self,
        provider_type: ProviderType | str | None = None,
        model_id: str | None = None,
        max_retries: int = 3,
    ) -> AIProviderInterface:
        """Create an AIProviderInterface instance for a provider.

        All providers are wrapped uniformly in ``LangChainChatProvider``.
        Raw LLM instantiation is delegated to ``_create_provider_instance()``
        so that adding a new provider requires touching exactly one place.

        Args:
            provider_type: The provider type. Uses default if not specified.
            model_id: The model ID. Uses default for provider if not specified.
            max_retries: Maximum retry attempts for failed operations.

        Returns:
            An AIProviderInterface implementation.

        Raises:
            ValueError: If no provider is configured or provider is invalid.
        """
        from cleveragents.providers.llm.langchain_chat_provider import (
            LangChainChatProvider,
        )

        resolved = self._resolve_provider_type(provider_type)

        if model_id is None:
            model_id = self.get_default_model(resolved)

        provider_info = self.get_provider_info(resolved)
        capabilities = (
            provider_info.capabilities if provider_info else ProviderCapabilities()
        )

        # Eager validation: fail fast before building the factory closure,
        # then reuse the validated key to avoid a second lookup.
        api_key: str | None = self._get_api_key(resolved)

        def llm_factory(mid: str) -> BaseLanguageModel:
            factory_kwargs: dict[str, Any] = {"max_retries": max_retries}
            # Use sentinel so _create_provider_instance knows the key was
            # pre-validated and should not trigger a second _get_api_key.
            factory_kwargs["__api_key_sentinel"] = api_key
            return self._create_provider_instance(
                resolved,
                mid,
                **factory_kwargs,
            )

        return LangChainChatProvider(
            name=resolved.value,
            model_id=model_id or self.DEFAULT_MODELS.get(resolved, "unknown"),
            llm_factory=llm_factory,
            max_retries=max_retries,
            supports_streaming=capabilities.supports_streaming,
        )


def resolve_provider_by_name(
    name: str,
    settings: Settings | None = None,
) -> AIProviderInterface:
    """Resolve a provider by name with an explicit error when not configured.

    Args:
        name: Provider name (e.g. ``"openai"``, ``"anthropic"``).
        settings: Optional settings override.

    Returns:
        An ``AIProviderInterface`` implementation for the requested provider.

    Raises:
        ValueError: If the provider is not configured or the name is unknown.
    """
    registry = get_provider_registry(settings)
    try:
        provider_type = ProviderType(name.lower())
    except ValueError as exc:
        available = ", ".join(pt.value for pt in ProviderType)
        raise ValueError(
            f"Unknown provider '{name}'. Available providers: {available}"
        ) from exc

    if not registry.is_provider_configured(provider_type):
        raise ValueError(
            f"Provider '{name}' is not configured. "
            f"Set the required API key environment variable."
        )
    logger.debug("provider_resolved_by_name", provider=name)
    return registry.create_ai_provider(provider_type=provider_type)


# Global registry instance and its thread-safety lock
_registry: ProviderRegistry | None = None
_registry_lock: threading.Lock = threading.Lock()


def get_provider_registry(settings: Settings | None = None) -> ProviderRegistry:
    """Get the global provider registry instance.

    This function is thread-safe: all reads and writes to ``_registry`` are
    protected by ``_registry_lock``, ensuring only one ``ProviderRegistry``
    instance is ever created under concurrent access.

    Args:
        settings: Optional settings instance. Creates new registry if provided.

    Returns:
        The global ProviderRegistry instance.
    """
    global _registry
    with _registry_lock:
        if _registry is None or settings is not None:
            _registry = ProviderRegistry(settings)
        return _registry


def reset_provider_registry() -> None:
    """Reset the global provider registry.

    This function is thread-safe: the write to ``_registry`` is protected by
    ``_registry_lock``.

    Useful for testing to ensure clean state between tests.
    """
    global _registry
    with _registry_lock:
        _registry = None
