"""Embedding service with provider detection.

DEPRECATED: This module is maintained for backward compatibility.
New code should use nextcloud_mcp_server.providers.get_provider() directly.
"""

import logging

import anyio

from nextcloud_mcp_server.providers import get_provider

from .bm25_provider import BM25SparseEmbeddingProvider

logger = logging.getLogger(__name__)


class EmbeddingService:
    """
    Unified embedding service with automatic provider detection.

    DEPRECATED: This class wraps the new unified provider infrastructure
    for backward compatibility. New code should use
    nextcloud_mcp_server.providers.get_provider() directly.
    """

    def __init__(self):
        """Initialize embedding service with auto-detected provider."""
        self.provider = get_provider()

    async def embed(self, text: str) -> list[float]:
        """
        Generate embedding vector for text.

        Args:
            text: Input text to embed

        Returns:
            Vector embedding as list of floats
        """
        return await self.provider.embed(text)

    async def embed_batch(self, texts: list[str]) -> list[list[float]]:
        """
        Generate embeddings for multiple texts.

        Args:
            texts: List of texts to embed

        Returns:
            List of vector embeddings
        """
        return await self.provider.embed_batch(texts)

    async def embed_with_usage(self, text: str) -> tuple[list[float], int]:
        """Embed one text and report the request's token count.

        Returns ``(embedding, token_count)`` for usage metering (Deck #67).
        """
        return await self.provider.embed_with_usage(text)

    async def embed_batch_with_usage(
        self, texts: list[str]
    ) -> tuple[list[list[float]], int]:
        """Embed multiple texts and report the total token count.

        Returns ``(embeddings, token_count)`` for usage metering (Deck #67).
        """
        return await self.provider.embed_batch_with_usage(texts)

    def get_dimension(self) -> int:
        """
        Get embedding dimension.

        Returns:
            Vector dimension
        """
        return self.provider.get_dimension()

    async def close(self):
        """Close provider resources."""
        if hasattr(self.provider, "close") and callable(
            getattr(self.provider, "close")
        ):
            close_method = getattr(self.provider, "close")
            await close_method()


# Singleton instance
_embedding_service: EmbeddingService | None = None


def get_embedding_service() -> EmbeddingService:
    """
    Get singleton embedding service instance.

    Returns:
        Global EmbeddingService instance
    """
    global _embedding_service
    if _embedding_service is None:
        _embedding_service = EmbeddingService()
    return _embedding_service


# BM25 sparse embedding singleton
_bm25_service: BM25SparseEmbeddingProvider | None = None


async def get_bm25_service() -> BM25SparseEmbeddingProvider:
    """
    Get singleton BM25 sparse embedding service instance.

    Lazily instantiates the singleton off the event loop. The
    ``BM25SparseEmbeddingProvider`` constructor calls
    ``fastembed.SparseTextEmbedding(model_name="Qdrant/bm25")`` which
    downloads ~50 MB of model weights from HuggingFace and loads them
    into memory — observed >5 s wall-clock in production, enough to
    stall the calling thread. The encode methods on the provider
    already offload via ``anyio.to_thread.run_sync``; this routes the
    first-time init through the same path so the event loop stays
    responsive (kubernetes ``/health/live`` httpGet probe in
    particular).

    The singleton is process-wide so the per-pod cost is paid once.
    Subsequent calls hit the warm path and return after a single
    non-blocking await.

    Concurrent first callers race: two coroutines can both observe
    ``_bm25_service is None`` and both enter ``run_sync``. We accept
    the duplicate model load over an ``asyncio.Lock``, which has its
    own cross-loop hazards under anyio TaskGroups (see PR #799 for
    that class of bug). The duplicate is bounded — FastEmbed caches
    the downloaded weights on disk after the first call, so loser(s)
    pick up cheaply.

    Returns:
        Global BM25SparseEmbeddingProvider instance
    """
    global _bm25_service
    if _bm25_service is None:
        _bm25_service = await anyio.to_thread.run_sync(  # type: ignore[attr-defined]  # ty: ignore[unresolved-attribute]
            BM25SparseEmbeddingProvider
        )
    return _bm25_service
