"""Sentence Transformers embedding provider."""

from typing import Any, Dict, List, Optional

from langchain_core.embeddings import Embeddings
from loguru import logger

from ....config.thread_settings import get_setting_from_snapshot
from ..base import BaseEmbeddingProvider


class SentenceTransformersProvider(BaseEmbeddingProvider):
    """
    Sentence Transformers embedding provider.

    Uses HuggingFace sentence-transformers models for local embeddings.
    No API key required, runs entirely locally.
    """

    provider_name = "Sentence Transformers"
    provider_key = "SENTENCE_TRANSFORMERS"
    requires_api_key = False
    supports_local = True
    default_model = "all-MiniLM-L6-v2"  # type: ignore[assignment]

    # Available models with metadata
    AVAILABLE_MODELS = {
        "all-MiniLM-L6-v2": {
            "dimensions": 384,
            "description": "Fast, lightweight model. Good for general use.",
            "max_seq_length": 256,
        },
        "all-mpnet-base-v2": {
            "dimensions": 768,
            "description": "Higher quality, slower. Best accuracy.",
            "max_seq_length": 384,
        },
        "multi-qa-MiniLM-L6-cos-v1": {
            "dimensions": 384,
            "description": "Optimized for question-answering tasks.",
            "max_seq_length": 512,
        },
        "paraphrase-multilingual-MiniLM-L12-v2": {
            "dimensions": 384,
            "description": "Supports multiple languages.",
            "max_seq_length": 128,
        },
    }

    @classmethod
    def create_embeddings(
        cls,
        model: Optional[str] = None,
        settings_snapshot: Optional[Dict[str, Any]] = None,
        **kwargs,
    ) -> Embeddings:
        """
        Create Sentence Transformers embeddings instance.

        Args:
            model: Model name (defaults to all-MiniLM-L6-v2)
            settings_snapshot: Optional settings snapshot
            **kwargs: Additional parameters (device, etc.)

        Returns:
            SentenceTransformerEmbeddings instance
        """
        from langchain_community.embeddings import (
            SentenceTransformerEmbeddings,
        )

        # Get model from settings if not specified
        if model is None:
            model = get_setting_from_snapshot(
                "embeddings.sentence_transformers.model",
                default=cls.default_model,
                settings_snapshot=settings_snapshot,
            )

        # Get device setting (cpu or cuda)
        device = kwargs.get("device")
        if device is None:
            device = get_setting_from_snapshot(
                "embeddings.sentence_transformers.device",
                default="cpu",
                settings_snapshot=settings_snapshot,
            )

        logger.info(
            f"Creating SentenceTransformerEmbeddings with model={model}, device={device}"
        )

        # Egress policy: if the user opted into local-only embeddings,
        # refuse to trigger a HuggingFace download on first use. The
        # SentenceTransformer constructor reaches out to huggingface.co
        # when the requested model isn't cached locally — silent
        # outbound traffic that violates ``embeddings.require_local=True``.
        #
        # Resolve the requirement through the egress CONTEXT, not the raw
        # ``embeddings.require_local`` flag: under PRIVATE_ONLY,
        # context_from_snapshot forces require_local_embeddings=True even
        # when the user left the flag at its default False. Reading the raw
        # flag here would let a PRIVATE_ONLY (offline) run silently download
        # an uncached model from HuggingFace. An unknown/corrupt scope
        # raises PolicyDeniedError out of context_from_snapshot — fail
        # closed, do not download.
        require_local = False
        if settings_snapshot is not None:
            try:
                from ....security.egress.policy import (
                    context_from_snapshot,
                    resolve_run_primary_engine,
                )

                # Single source of truth for the primary (was: search.tool +
                # searxng fallback, a fail-OPEN that could permit a remote model
                # download for a primary-less private run).
                _primary = resolve_run_primary_engine(settings_snapshot)
                require_local = context_from_snapshot(
                    settings_snapshot, _primary
                ).require_local_embeddings
            except ValueError:
                # No usable primary / invalid scope: fail CLOSED to local-only
                # (block any remote model download) rather than reading the raw
                # opt-in flag. The get_embeddings PEP already refuses a
                # primary-less snapshot upstream, so this is defense-in-depth.
                require_local = True
        model_kwargs = {"device": device}
        if require_local:
            if not cls._is_model_cached_locally(model):
                from ....security.egress.policy import (
                    Decision,
                    PolicyDeniedError,
                )

                logger.bind(policy_audit=True).warning(
                    "refusing SentenceTransformer download under "
                    "embeddings.require_local=True",
                    model=model,
                )
                raise PolicyDeniedError(
                    Decision(False, "embeddings_model_not_cached"),
                    target=model,
                )
            # Force the inner ``transformers``/``sentence_transformers``
            # call to use cached files only — defence in depth in case
            # the cache check above misses an alternative cache path.
            model_kwargs["local_files_only"] = True

        return SentenceTransformerEmbeddings(
            model_name=model,
            model_kwargs=model_kwargs,
        )

    @staticmethod
    def _is_model_cached_locally(model_name: str) -> bool:
        """Best-effort check whether ``model_name`` is already cached.

        Looks at the HuggingFace hub cache directory; returns False if
        the cache lookup itself fails, which fails closed under
        ``require_local=True``.
        """
        try:
            from huggingface_hub import try_to_load_from_cache

            # try_to_load_from_cache returns a path string when cached,
            # None when missing, and the sentinel _CACHED_NO_EXIST for
            # known-absent. Treat anything but a string path as a miss.
            cached = try_to_load_from_cache(
                repo_id=model_name, filename="config.json"
            )
            return isinstance(cached, str) and bool(cached)
        except Exception:  # pragma: no cover - defensive
            return False

    @classmethod
    def is_available(
        cls, settings_snapshot: Optional[Dict[str, Any]] = None
    ) -> bool:
        """
        Check if Sentence Transformers is available.

        Since sentence-transformers is a required dependency, this always returns True.
        This method exists for API consistency with other providers.
        """
        return True

    @classmethod
    def get_available_models(
        cls, settings_snapshot: Optional[Dict[str, Any]] = None
    ) -> List[Dict[str, Any]]:
        """
        Get list of available Sentence Transformer models.

        Note: Since there's no centralized API for Sentence Transformers,
        we return a curated list of commonly used models. Users can also
        specify any model name from HuggingFace directly in settings.
        """
        return [
            {
                "value": model,
                "label": f"{model} ({info['dimensions']}d) - {info['description']}",
            }
            for model, info in cls.AVAILABLE_MODELS.items()
        ]
