"""Ollama embedding provider."""

import weakref
from typing import Any, Dict, List, Optional

from langchain_ollama import OllamaEmbeddings
from langchain_core.embeddings import Embeddings
from loguru import logger

from ....config.thread_settings import get_setting_from_snapshot
from ....utilities.llm_utils import (
    _close_inner_ollama_clients,
    get_ollama_base_url,
)
from ..base import BaseEmbeddingProvider
from ....security import safe_get, safe_post


class OllamaEmbeddingsProvider(BaseEmbeddingProvider):
    """
    Ollama embedding provider.

    Uses Ollama API for local embedding models.
    No API key required, runs locally.
    """

    provider_name = "Ollama"
    provider_key = "OLLAMA"
    requires_api_key = False
    supports_local = True
    default_model = "nomic-embed-text"  # type: ignore[assignment]

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

        Args:
            model: Model name (defaults to nomic-embed-text)
            settings_snapshot: Optional settings snapshot
            **kwargs: Additional parameters (base_url, etc.)

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

        # Get Ollama URL
        base_url = kwargs.get("base_url")
        if base_url is None:
            base_url = get_ollama_base_url(settings_snapshot)

        # Without an explicit num_ctx, Ollama uses the model's modelfile
        # default (often 2048). Inputs longer than that return HTTP 500
        # ("input length exceeds the context length") rather than being
        # truncated, which aborts indexing mid-batch.
        num_ctx = get_setting_from_snapshot(
            "embeddings.ollama.num_ctx",
            default=8192,
            settings_snapshot=settings_snapshot,
        )

        logger.info(
            f"Creating OllamaEmbeddings with model={model}, "
            f"base_url={base_url}, num_ctx={num_ctx}"
        )

        ollama_kwargs: Dict[str, Any] = {
            "model": model,
            "base_url": base_url,
        }
        if num_ctx:
            ollama_kwargs["num_ctx"] = int(num_ctx)

        instance = OllamaEmbeddings(**ollama_kwargs)

        # Safety net for callers that bypass LocalEmbeddingManager (e.g.,
        # the programmatic-API examples in examples/api_usage, direct
        # constructions in test fixtures). The manager-driven explicit
        # close remains the load-bearing primary path; this finalizer
        # only fires when the instance is GC'd without an explicit
        # close. We pass the inner sync/async ``ollama.Client`` objects
        # rather than ``instance`` itself — a strong reference back to
        # the wrapping instance would defeat the finalizer's purpose by
        # keeping the instance alive forever.
        try:
            weakref.finalize(
                instance,
                _close_inner_ollama_clients,
                instance._client,
                instance._async_client,
            )
        except AttributeError:
            # Future langchain_ollama versions may reshape the private
            # attrs; don't crash the factory if the introspection misses.
            logger.debug(
                "OllamaEmbeddings shape changed — finalizer not registered"
            )

        return instance

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

            # Get Ollama URL
            base_url = get_ollama_base_url(settings_snapshot)

            # Check if Ollama is running
            try:
                response = safe_get(
                    f"{base_url}/api/tags",
                    timeout=3,
                    allow_localhost=True,
                    allow_private_ips=True,
                )
                return response.status_code == 200
            except requests.exceptions.RequestException:
                return False

        except Exception:
            logger.exception("Error checking Ollama availability")
            return False

    @classmethod
    def _get_model_capabilities(
        cls, base_url: str, model_name: str
    ) -> Optional[List[str]]:
        """Query Ollama /api/show for a model's capabilities.

        Returns the capabilities list (e.g. ["embedding"]) or None on failure.
        """
        try:
            response = safe_post(
                f"{base_url}/api/show",
                json={"model": model_name},
                timeout=5,
                allow_localhost=True,
                allow_private_ips=True,
            )
            if response.status_code == 200:
                return response.json().get("capabilities")  # type: ignore[no-any-return]
        except Exception:
            logger.debug(f"Could not fetch capabilities for {model_name}")
        return None

    @classmethod
    def is_embedding_model(
        cls,
        model: str,
        settings_snapshot: Optional[Dict[str, Any]] = None,
    ) -> Optional[bool]:
        """Check whether an Ollama model supports embeddings.

        Uses the /api/show capabilities field. Returns ``None`` when the
        capability list isn't available (older Ollama servers) — the
        provider doesn't guess from the model name. Callers must treat
        ``None`` as "unknown", not as "no", so models stay listed even
        when their capability can't be confirmed.
        """
        base_url = get_ollama_base_url(settings_snapshot)
        caps = cls._get_model_capabilities(base_url, model)

        # No name-based fallback on purpose — see method docstring.
        if caps is None:
            return None
        return "embedding" in caps

    @classmethod
    def get_available_models(
        cls, settings_snapshot: Optional[Dict[str, Any]] = None
    ) -> List[Dict[str, Any]]:
        """Get all Ollama models, tagged when /api/show reports support.

        No filtering on the model list itself — every model the Ollama
        server reports is returned. We only *tag* entries with
        ``is_embedding`` when ``/api/show`` exposes a real capabilities
        list (so the UI can sort them); we don't guess from the model
        name. Older Ollama servers without capabilities → models are
        returned untagged and the user decides.
        """
        from ....utilities.llm_utils import fetch_ollama_models

        base_url = get_ollama_base_url(settings_snapshot)
        # fetch_ollama_models returns every installed model. We pass it
        # through unfiltered — no name heuristic, no exclusions.
        all_models = fetch_ollama_models(base_url, timeout=3.0)

        if not all_models:
            return []

        embedding_models: List[Dict[str, Any]] = []
        untagged_models: List[Dict[str, Any]] = []
        other_models: List[Dict[str, Any]] = []

        for model in all_models:
            model_name = model["value"]
            caps = cls._get_model_capabilities(base_url, model_name)

            entry: Dict[str, Any] = dict(model)
            if caps is None:
                # No capability signal from the server → don't guess.
                # Keep the model in the list so the user can still
                # select it.
                untagged_models.append(entry)
                continue

            # /api/show capabilities is an API-driven signal (not a
            # name match), so it's safe to use for the flag.
            is_embed = "embedding" in caps
            entry["is_embedding"] = is_embed
            if is_embed:
                embedding_models.append(entry)
            else:
                other_models.append(entry)

        logger.info(
            "Found {} embedding-capable, {} non-embedding, and {} "
            "untagged models from Ollama",
            len(embedding_models),
            len(other_models),
            len(untagged_models),
        )

        # Embedding-tagged first so they're the default pick; untagged
        # next (capability unknown — user decides); then explicit
        # non-embedding. Nothing is dropped.
        return embedding_models + untagged_models + other_models
