"""OpenAI embedding provider."""

from typing import Any, Dict, List, Optional
from urllib.parse import urlparse

from langchain_core.embeddings import Embeddings
from loguru import logger

from ....config.thread_settings import get_setting_from_snapshot
from ....security.log_sanitizer import redact_secrets
from ....utilities.url_utils import normalize_url
from ..base import BaseEmbeddingProvider


class OpenAIEmbeddingsProvider(BaseEmbeddingProvider):
    """
    OpenAI embedding provider.

    Targets the OpenAI cloud API by default, and any OpenAI-compatible
    endpoint (LM Studio, vLLM, llama.cpp server, etc.) when
    ``embeddings.openai.base_url`` is configured. An API key is required
    for the cloud, but optional for keyless local servers — the
    ``base_url``-set, ``api_key``-empty configuration falls back to a
    placeholder key so the OpenAI client request still goes out.
    """

    provider_name = "OpenAI"
    provider_key = "OPENAI"
    # Not strictly required: the OpenAI cloud needs a key, but
    # OpenAI-compatible local servers (LM Studio, vLLM, llama.cpp)
    # don't. ``is_available`` and ``create_embeddings`` enforce the
    # cloud-needs-key rule at runtime when no base_url is set.
    # Inherits ``requires_api_key = False`` from BaseEmbeddingProvider.
    supports_local = False
    default_model = "text-embedding-3-small"  # type: ignore[assignment]
    # Placeholder key used when targeting an OpenAI-compatible local
    # server (api_key empty, base_url set). Mirrors the LLM-side
    # LMStudio provider's keyless-fallback pattern.
    _PLACEHOLDER_API_KEY = "lm-studio"

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

        Args:
            model: Model name (defaults to text-embedding-3-small)
            settings_snapshot: Optional settings snapshot
            **kwargs: Additional parameters (api_key, etc.)

        Returns:
            OpenAIEmbeddings instance

        Raises:
            ValueError: If API key is not configured
        """
        from langchain_openai import OpenAIEmbeddings

        # Get API key + base_url. Read base_url first so we can decide
        # whether a missing api_key is fatal (cloud) or just a keyless
        # local-server signal (OpenAI-compatible endpoint).
        base_url = kwargs.get("base_url")
        if base_url is None:
            base_url = get_setting_from_snapshot(
                "embeddings.openai.base_url",
                default=None,
                settings_snapshot=settings_snapshot,
            )

        api_key = kwargs.get("api_key")
        if api_key is None:
            api_key = get_setting_from_snapshot(
                "embeddings.openai.api_key",
                default=None,
                settings_snapshot=settings_snapshot,
            )

        if not api_key:
            if base_url:
                # OpenAI-compatible local server (LM Studio, vLLM,
                # llama.cpp). The server ignores the key but the
                # OpenAI client requires the field to be non-empty.
                logger.info(
                    "OpenAI embeddings: no API key set but base_url={} "
                    "is configured — using placeholder key for the "
                    "OpenAI-compatible endpoint.",
                    base_url,
                )
                api_key = cls._PLACEHOLDER_API_KEY
            else:
                logger.error("OpenAI API key not found in settings")
                raise ValueError(
                    "OpenAI API key not configured. "
                    "Please set embeddings.openai.api_key in settings, "
                    "or set embeddings.openai.base_url to point at an "
                    "OpenAI-compatible local server."
                )

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

        dimensions = kwargs.get("dimensions")
        if dimensions is None:
            dimensions = get_setting_from_snapshot(
                "embeddings.openai.dimensions",
                default=None,
                settings_snapshot=settings_snapshot,
            )

        logger.info(f"Creating OpenAIEmbeddings with model={model}")

        # Build parameters. Annotated as Dict[str, Any] so the
        # heterogeneous values (str for model/key/base_url, int for
        # dimensions) and the **params unpack into OpenAIEmbeddings
        # type-check under mypy.
        params: Dict[str, Any] = {
            "model": model,
            "openai_api_key": api_key,
        }

        if base_url:
            # Normalize first so a scheme-less entry like "api.openai.com"
            # parses to a hostname (urlparse otherwise returns hostname=None
            # for bare hosts, which would silently drop the ctx-length guard
            # for the real OpenAI endpoint). Mirrors the LLM-side OpenAI
            # provider, which already normalizes via the same helper.
            base_url = normalize_url(base_url)
            params["openai_api_base"] = base_url
            # Disable client-side context length checks only for non-OpenAI
            # hosts (LM Studio, vLLM, llama.cpp, etc.) which may lack tiktoken
            # model entries or reject tokenized inputs. Keep the LangChain
            # default for api.openai.com so the guard stays in place for users
            # who set base_url explicitly to the real OpenAI endpoint.
            if urlparse(base_url).hostname != "api.openai.com":
                params["check_embedding_ctx_length"] = False

        # For text-embedding-3 models, dimensions can be customized
        if dimensions and model.startswith("text-embedding-3"):
            params["dimensions"] = int(dimensions)

        return OpenAIEmbeddings(**params)

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

        Available when either an API key (cloud) or a custom base URL
        (OpenAI-compatible local server) is configured. A blank
        installation still reports unavailable so the UI doesn't list
        the provider on first launch.
        """
        api_key = None
        try:
            api_key = get_setting_from_snapshot(
                "embeddings.openai.api_key",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            if api_key and str(api_key).strip():
                return True
            base_url = get_setting_from_snapshot(
                "embeddings.openai.base_url",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            return bool(base_url and str(base_url).strip())
        except Exception as e:
            # Drop exc_info — the traceback would render the exception's
            # cause chain, which may embed the api_key value if a
            # settings-layer error message ever surfaces it. Interpolate a
            # redacted exception message so debugging is still possible.
            safe_msg = redact_secrets(str(e), api_key)
            logger.debug(
                f"Error checking OpenAI embedding availability: {safe_msg}"
            )
            return False

    @classmethod
    def get_available_models(
        cls, settings_snapshot: Optional[Dict[str, Any]] = None
    ) -> List[Dict[str, Any]]:
        """Get every model the configured endpoint reports.

        No filtering: ``/v1/models`` doesn't expose a reliable "is this
        an embedding model?" signal — neither cloud OpenAI nor
        OpenAI-compatible local servers (LM Studio, vLLM, llama.cpp).
        Earlier versions guessed from the model name and ended up
        hiding real embedding models whose names didn't match the
        heuristic (e.g. ``nomic-embed-text-v1.5`` was dropped because
        it lacks the trailing ``-ing``). The dropdown now shows every
        model the endpoint returns so the user can pick the one they
        actually loaded.
        """
        # Initialized before the try so the except block can redact it
        # even if an early statement (e.g. the openai import) raises.
        api_key = None
        try:
            from openai import OpenAI

            api_key = get_setting_from_snapshot(
                "embeddings.openai.api_key",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            base_url = get_setting_from_snapshot(
                "embeddings.openai.base_url",
                default=None,
                settings_snapshot=settings_snapshot,
            )

            if not api_key:
                if base_url:
                    # Keyless OpenAI-compatible local server — use a
                    # placeholder so the client request can proceed.
                    api_key = cls._PLACEHOLDER_API_KEY
                else:
                    logger.warning("OpenAI API key not configured")
                    return []

            client_kwargs: Dict[str, Any] = {"api_key": api_key}
            if base_url:
                client_kwargs["base_url"] = normalize_url(base_url)
            client = OpenAI(**client_kwargs)
            models_response = client.models.list()

            # No name-based filtering — see method docstring (#4195).
            models: List[Dict[str, Any]] = []
            for model in models_response.data:
                model_id = model.id
                # Skip only blank ids (malformed entry); never skip
                # based on what the name looks like.
                if not model_id:
                    continue
                models.append({"value": model_id, "label": model_id})

            logger.info(
                "Fetched {} models from OpenAI endpoint{}",
                len(models),
                f" at {base_url}" if base_url else "",
            )
            return models

        except Exception as e:
            # Use logger.warning rather than logger.exception so the
            # exception's cause chain (which may embed the api_key in a
            # URL or auth header echoed back in the error body) is not
            # written to log sinks. The api_key value is also redacted
            # from str(e) as defense-in-depth.
            safe_msg = redact_secrets(str(e), api_key)
            logger.warning(
                f"Error fetching OpenAI embedding models: {safe_msg}"
            )
            return []
