"""
Central configuration for embedding providers.

This module provides the main get_embeddings() function and availability checks
for different embedding providers, similar to llm_config.py.
"""

from typing import Any, Dict, Optional, Type

from langchain_core.embeddings import Embeddings
from loguru import logger

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

# Internal: list of provider strings accepted by get_embeddings().
# Not re-exported from embeddings/__init__.py — call sites should not import it.
# Kept module-level so the validation error message at L~163 can list options.
VALID_EMBEDDING_PROVIDERS = [
    "sentence_transformers",
    "ollama",
    "openai",
]

# Lazy-loaded provider classes dict
_PROVIDER_CLASSES: Optional[Dict[str, Type[BaseEmbeddingProvider]]] = None


def _get_provider_classes() -> Dict[str, Type[BaseEmbeddingProvider]]:
    """Lazy load provider classes to avoid circular imports."""
    global _PROVIDER_CLASSES
    if _PROVIDER_CLASSES is None:
        from .providers.implementations.sentence_transformers import (
            SentenceTransformersProvider,
        )
        from .providers.implementations.ollama import OllamaEmbeddingsProvider
        from .providers.implementations.openai import OpenAIEmbeddingsProvider

        _PROVIDER_CLASSES = {
            "sentence_transformers": SentenceTransformersProvider,
            "ollama": OllamaEmbeddingsProvider,
            "openai": OpenAIEmbeddingsProvider,
        }
    return _PROVIDER_CLASSES


def is_sentence_transformers_available() -> bool:
    """Check if Sentence Transformers is available."""
    provider_classes = _get_provider_classes()
    return provider_classes["sentence_transformers"].is_available()


def is_ollama_embeddings_available(
    settings_snapshot: Optional[Dict[str, Any]] = None,
) -> bool:
    """Check if Ollama embeddings are available."""
    provider_classes = _get_provider_classes()
    return provider_classes["ollama"].is_available(settings_snapshot)


def is_openai_embeddings_available(
    settings_snapshot: Optional[Dict[str, Any]] = None,
) -> bool:
    """Check if OpenAI embeddings are available."""
    provider_classes = _get_provider_classes()
    return provider_classes["openai"].is_available(settings_snapshot)


def get_available_embedding_providers(
    settings_snapshot: Optional[Dict[str, Any]] = None,
) -> Dict[str, str]:
    """
    Return available embedding providers.

    Args:
        settings_snapshot: Optional settings snapshot

    Returns:
        Dict mapping provider keys to display names
    """
    providers = {}

    if is_sentence_transformers_available():
        providers["sentence_transformers"] = "Sentence Transformers (Local)"

    if is_ollama_embeddings_available(settings_snapshot):
        providers["ollama"] = "Ollama (Local)"

    if is_openai_embeddings_available(settings_snapshot):
        # Single entry covers the OpenAI cloud API and any
        # OpenAI-compatible endpoint (LM Studio, vLLM, llama.cpp);
        # the provider class branches on
        # ``embeddings.openai.base_url`` at runtime.
        providers["openai"] = "OpenAI / OpenAI-Compatible Endpoint"

    return providers


def get_embedding_function(
    provider: Optional[str] = None,
    model_name: Optional[str] = None,
    settings_snapshot: Optional[Dict[str, Any]] = None,
    **kwargs,
):
    """
    Get a callable embedding function that can embed texts.

    Args:
        provider: Embedding provider to use
        model_name: Model name to use
        settings_snapshot: Optional settings snapshot
        **kwargs: Additional provider-specific parameters

    Returns:
        A callable that takes a list of texts and returns embeddings
    """
    embeddings = get_embeddings(
        provider=provider,
        model=model_name,
        settings_snapshot=settings_snapshot,
        **kwargs,
    )
    return embeddings.embed_documents


def get_embeddings(
    provider: Optional[str] = None,
    model: Optional[str] = None,
    settings_snapshot: Optional[Dict[str, Any]] = None,
    **kwargs,
) -> Embeddings:
    """
    Get embeddings instance based on provider and model.

    Args:
        provider: Embedding provider to use (if None, uses settings)
        model: Model name to use (if None, uses settings or provider default)
        settings_snapshot: Optional settings snapshot for thread-safe access
        **kwargs: Additional provider-specific parameters

    Returns:
        A LangChain Embeddings instance

    Raises:
        ValueError: If provider is invalid or not available
        ImportError: If required dependencies are not installed
    """
    # Get provider from settings if not specified
    if provider is None:
        provider = get_setting_from_snapshot(
            "embeddings.provider",
            default="sentence_transformers",
            settings_snapshot=settings_snapshot,
        )

    # Clean and normalize provider
    if provider:
        provider = provider.strip().strip("\"'").strip().lower()

    # Validate provider
    if provider not in VALID_EMBEDDING_PROVIDERS:
        logger.error(f"Invalid embedding provider: {provider}")
        raise ValueError(
            f"Invalid embedding provider: {provider}. "
            f"Must be one of: {VALID_EMBEDDING_PROVIDERS}"
        )

    logger.info(f"Getting embeddings with provider: {provider}, model: {model}")

    # Egress policy PEP: centralized here so callers that didn't run
    # the upstream RAG/library preflight checks still get gated.
    # Previously the PEP existed only in two callers
    # (rag_service_factory + library_rag_service.__init__) — any third
    # caller (e.g. /api/rag/test-embedding) bypassed it.
    if settings_snapshot is None:
        # Snapshot-less path: the embeddings.require_local toggle can't be
        # read, so we can't honour an opt-in restriction. Fail closed by
        # only allowing localhost-default providers — same contract as the
        # LLM gate in config/llm_config.py. A snapshot-less get_embeddings(
        # provider="openai") must NOT silently ship the local corpus to a
        # cloud embedder.
        from ..security.egress.policy import (
            _LOCAL_DEFAULT_EMBEDDING_PROVIDERS,
            Decision,
            PolicyDeniedError,
        )

        if provider not in _LOCAL_DEFAULT_EMBEDDING_PROVIDERS:
            logger.bind(policy_audit=True).warning(
                "embeddings denied: non-local provider with no settings "
                "snapshot to evaluate policy",
                provider=provider,
            )
            raise PolicyDeniedError(
                Decision(False, "no_snapshot_for_provider"),
                target=f"embeddings:{provider}",
            )
    else:
        from ..security.egress.policy import (
            Decision,
            PolicyDeniedError,
            context_from_snapshot,
            evaluate_embeddings,
            resolve_run_primary_engine,
        )

        try:
            # Single source of truth for the primary (was: search.tool +
            # searxng fallback, a fail-OPEN — a missing primary defaulted to
            # the public searxng -> PUBLIC_ONLY -> require_local_embeddings
            # stayed False -> the corpus could ship to a cloud embedder for a
            # private run). A missing/invalid primary now raises -> fail closed.
            primary = resolve_run_primary_engine(settings_snapshot)
            ctx = context_from_snapshot(settings_snapshot, primary)
        except PolicyDeniedError:
            raise
        except ValueError as exc:
            raise PolicyDeniedError(
                Decision(False, "invalid_policy_config"),
                target=f"embeddings:{provider}",
            ) from exc
        # Gate on the SCOPE-AWARE requirement, 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 miss that — a PRIVATE_ONLY run with the
        # default flag would ship the corpus to a cloud embedder.
        if ctx.require_local_embeddings:
            decision = evaluate_embeddings(
                provider, ctx, settings_snapshot=settings_snapshot
            )
            if not decision.allowed:
                logger.bind(policy_audit=True).warning(
                    "embeddings denied by egress policy",
                    provider=provider,
                    reason=decision.reason,
                )
                raise PolicyDeniedError(
                    decision, target=f"embeddings:{provider}"
                )

    # Get provider class and create embeddings
    provider_classes = _get_provider_classes()
    provider_class = provider_classes.get(provider)

    if not provider_class:
        raise ValueError(f"Unsupported embedding provider: {provider}")

    return provider_class.create_embeddings(
        model=model, settings_snapshot=settings_snapshot, **kwargs
    )
