"""Unified LLM Client: Adapter zwischen OllamaClient-Interface und LLMBackend.

Löst das Verdrahtungsproblem: Planner, Reflector und Gateway nutzen
alle `self._ollama.chat()` mit Ollama-spezifischem Response-Format.
Dieser Adapter stellt dasselbe Interface bereit, leitet aber an
das konfigurierte LLMBackend weiter.

Usage:
    # Gateway erstellt den Client basierend auf Config:
    client = UnifiedLLMClient.create(config)

    # Planner/Reflector nutzen ihn wie bisher:
    response = await client.chat(model="qwen3:32b", messages=[...])
    text = response.get("message", {}).get("content", "")
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from jarvis.core.llm_backend import LLMBackendError
from jarvis.core.model_router import OllamaClient, OllamaError
from jarvis.utils.logging import get_logger

if TYPE_CHECKING:
    from collections.abc import AsyncIterator

    from jarvis.config import JarvisConfig

log = get_logger(__name__)


class UnifiedLLMClient:
    """Adapter der das OllamaClient-Interface auf beliebige LLM-Backends mappt.

    Gibt Responses immer im Ollama-Dict-Format zurück, damit
    Planner/Reflector/etc. nicht geändert werden müssen.

    Unterstützt: Ollama (direkt), OpenAI, Anthropic (via LLMBackend).
    """

    def __init__(
        self,
        ollama_client: OllamaClient | None,
        backend: Any | None = None,
        config: JarvisConfig | None = None,
    ) -> None:
        """Erstellt den unified Client.

        Args:
            ollama_client: Optionaler OllamaClient (nur bei Ollama-Modus oder Fallback).
            backend: Optionales LLMBackend aus llm_backend.py.
                     Wenn None und ollama_client vorhanden, wird direkt OllamaClient genutzt.
            config: JarvisConfig für on-demand per-task backend creation.
        """
        self._ollama = ollama_client
        self._backend = backend
        self._config = config
        self._backend_type: str = "ollama"
        self._backend_cache: dict[str, Any] = {}

        if backend is not None:
            self._backend_type = getattr(backend, "backend_type", "unknown")
            if hasattr(self._backend_type, "value"):
                self._backend_type = self._backend_type.value

    @classmethod
    def create(cls, config: JarvisConfig) -> UnifiedLLMClient:
        """Factory: Erstellt den passenden Client basierend auf der Config.

        Args:
            config: Jarvis-Konfiguration mit llm_backend_type.

        Returns:
            Konfigurierter UnifiedLLMClient.
        """
        backend = None
        ollama_client: OllamaClient | None = None

        if config.llm_backend_type != "ollama":
            try:
                from jarvis.core.llm_backend import create_backend

                backend = create_backend(config)
                log.info(
                    "unified_client_created",
                    backend=config.llm_backend_type,
                )
            except Exception as exc:
                log.warning(
                    "llm_backend_creation_failed",
                    backend=config.llm_backend_type,
                    error=str(exc),
                    fallback="ollama",
                )
                backend = None
                # Backend-Erstellung fehlgeschlagen → Ollama als Fallback
                ollama_client = OllamaClient(config)
        else:
            # Ollama-Modus: OllamaClient erstellen
            ollama_client = OllamaClient(config)

        return cls(ollama_client, backend, config=config)

    # ========================================================================
    # Per-task backend resolution
    # ========================================================================

    def _lookup_backend_for_model(self, model: str) -> str:
        """Check if any ModelConfig.backend is set for the given model name."""
        if self._config is None:
            return ""
        for role in ("planner", "executor", "coder", "coder_fast", "embedding"):
            cfg = getattr(self._config.models, role, None)
            if cfg is not None and cfg.name == model:
                return getattr(cfg, "backend", "") or ""
        return ""

    def _resolve_backend(self, backend_override: str) -> Any | None:
        """Returns the LLMBackend for a per-task override, or None for Ollama.

        Backends are lazily created and cached by provider name so that
        repeated calls for the same provider reuse the connection.
        """
        if not backend_override or backend_override == "ollama":
            return None  # use Ollama path

        # If the override matches the global backend, reuse it
        if self._backend is not None and backend_override == self._backend_type:
            return self._backend

        # Lazy-create and cache
        if backend_override in self._backend_cache:
            return self._backend_cache[backend_override]

        if self._config is None:
            log.warning("per_task_backend_no_config", override=backend_override)
            return self._backend

        try:
            from jarvis.core.llm_backend import create_backend

            # Temporarily override the backend type in a copy
            temp_config = self._config.model_copy(update={"llm_backend_type": backend_override})
            new_backend = create_backend(temp_config)
            self._backend_cache[backend_override] = new_backend
            log.info(
                "per_task_backend_created",
                provider=backend_override,
            )
            return new_backend
        except Exception as exc:
            log.warning(
                "per_task_backend_failed",
                provider=backend_override,
                error=str(exc),
                fallback="global",
            )
            return self._backend

    # ========================================================================
    # Chat (Hauptmethode -- von Planner/Reflector aufgerufen)
    # ========================================================================

    async def chat(
        self,
        model: str,
        messages: list[dict[str, Any]],
        *,
        tools: list[dict[str, Any]] | None = None,
        temperature: float = 0.7,
        top_p: float = 0.9,
        stream: bool = False,
        format_json: bool = False,
        options: dict[str, Any] | None = None,
        backend_override: str = "",
    ) -> dict[str, Any]:
        """Chat-Completion im Ollama-Response-Format.

        Leitet an das konfigurierte Backend weiter und konvertiert
        die Antwort in das Ollama-Dict-Format:

            {
                "message": {
                    "role": "assistant",
                    "content": "...",
                    "tool_calls": [...]
                },
                "model": "...",
                "done": true
            }

        Args:
            backend_override: Per-task backend provider name (e.g. "openai").
                Empty string uses the global backend.

        Raises:
            OllamaError: Bei jedem Backend-Fehler (einheitliche Exception).
        """
        # Resolve per-task backend: explicit override > model-config lookup > global
        if not backend_override and self._config is not None:
            backend_override = self._lookup_backend_for_model(model)
        effective_backend = (
            self._resolve_backend(backend_override) if backend_override else self._backend
        )

        if effective_backend is None:
            # Direkt an OllamaClient weiterleiten
            if self._ollama is None:
                raise OllamaError("Kein LLM-Backend verfügbar (weder API noch Ollama)")
            return await self._ollama.chat(
                model=model,
                messages=messages,
                tools=tools,
                temperature=temperature,
                top_p=top_p,
                stream=stream,
                format_json=format_json,
                options=options,
            )

        # Via LLMBackend
        try:
            response = await effective_backend.chat(
                model=model,
                messages=messages,
                tools=tools,
                temperature=temperature,
                top_p=top_p,
                format_json=format_json,
            )

            # ChatResponse → Ollama-Dict konvertieren
            result: dict[str, Any] = {
                "message": {
                    "role": "assistant",
                    "content": response.content,
                },
                "model": response.model or model,
                "done": True,
            }

            # Tool-Calls übernehmen
            if response.tool_calls:
                result["message"]["tool_calls"] = response.tool_calls

            # Usage-Info übernehmen
            if response.usage:
                result["prompt_eval_count"] = response.usage.get("prompt_tokens", 0)
                result["eval_count"] = response.usage.get("completion_tokens", 0)

            return result

        except Exception as exc:
            # Alle Backend-Fehler als OllamaError wrappen
            # damit Planner/Reflector catch-Blöcke weiter funktionieren
            bt = backend_override or self._backend_type
            raise OllamaError(
                f"LLM-Backend-Fehler ({bt}): {exc}",
                status_code=getattr(exc, "status_code", None),
            ) from exc

    # ========================================================================
    # Chat-Streaming
    # ========================================================================

    async def chat_stream(
        self,
        model: str,
        messages: list[dict[str, Any]],
        *,
        temperature: float = 0.7,
        top_p: float = 0.9,
    ) -> AsyncIterator[dict[str, Any]]:
        """Streaming-Chat im Ollama-Chunk-Format.

        Yields:
            Dicts im Format: {"message": {"content": "token"}, "done": false}
        """
        if self._backend is None:
            if self._ollama is None:
                raise OllamaError("Kein LLM-Backend verfügbar (weder API noch Ollama)")
            async for token in self._ollama.chat_stream(
                model=model,
                messages=messages,
                temperature=temperature,
                top_p=top_p,
            ):
                yield {
                    "message": {"role": "assistant", "content": token},
                    "done": False,
                }
            yield {"message": {"role": "assistant", "content": ""}, "done": True}
            return

        try:
            async for token in self._backend.chat_stream(
                model=model,
                messages=messages,
                temperature=temperature,
                top_p=top_p,
            ):
                yield {
                    "message": {"role": "assistant", "content": token},
                    "done": False,
                }

            # End-Marker
            yield {"message": {"role": "assistant", "content": ""}, "done": True}

        except Exception as exc:
            raise OllamaError(
                f"LLM-Stream-Fehler ({self._backend_type}): {exc}",
            ) from exc

    # ========================================================================
    # Embeddings
    # ========================================================================

    async def embed(self, model: str, text: str) -> dict[str, Any]:
        """Embedding im Ollama-Format: {"embedding": [0.1, 0.2, ...]}."""
        if self._backend is None:
            if self._ollama is None:
                raise OllamaError("Kein LLM-Backend verfügbar für Embeddings")
            vec = await self._ollama.embed(model, text)
            return {"embedding": vec} if not isinstance(vec, dict) else vec

        try:
            response = await self._backend.embed(model, text)
            return {"embedding": response.embedding}
        except (NotImplementedError, LLMBackendError):
            # Backend hat kein Embedding → Ollama-Fallback nur wenn verfügbar
            if self._ollama is not None:
                log.info("embedding_fallback_to_ollama", backend=self._backend_type)
                vec = await self._ollama.embed(model, text)
                return {"embedding": vec} if not isinstance(vec, dict) else vec
            raise
        except Exception as exc:
            raise OllamaError(f"Embedding-Fehler: {exc}") from exc

    async def batch_embed(self, model: str, texts: list[str]) -> list[dict[str, Any]]:
        """Batch-Embedding. Nutzt Backend wenn möglich, sonst OllamaClient."""
        if self._backend is None:
            if self._ollama is None:
                raise OllamaError("Kein LLM-Backend verfügbar für Embeddings")
            vecs = await self._ollama.embed_batch(model, texts)
            return [{"embedding": v} if not isinstance(v, dict) else v for v in vecs]

        # LLMBackend hat kein batch_embed → sequentiell
        results = []
        for text in texts:
            result = await self.embed(model, text)
            results.append(result)
        return results

    # ========================================================================
    # Meta-Methoden (von Gateway/ModelRouter benötigt)
    # ========================================================================

    async def is_available(self) -> bool:
        """Prüft ob das LLM-Backend erreichbar ist."""
        if self._backend is not None:
            try:
                return await self._backend.is_available()
            except Exception:
                return False
        if self._ollama is not None:
            return await self._ollama.is_available()
        return False

    async def list_models(self) -> list[str]:
        """Listet verfügbare Modelle."""
        if self._backend is not None:
            try:
                return await self._backend.list_models()
            except Exception:
                return []
        if self._ollama is not None:
            return await self._ollama.list_models()
        return []

    async def close(self) -> None:
        """Schließt alle Verbindungen."""
        if self._backend is not None:
            try:
                await self._backend.close()
            except Exception as exc:
                log.debug(
                    "backend_close_error", error=str(exc)
                )  # Cleanup — failure is non-critical
        if self._ollama is not None:
            await self._ollama.close()

    @property
    def backend_type(self) -> str:
        """Gibt den aktiven Backend-Typ zurück."""
        return self._backend_type

    @property
    def has_embedding_support(self) -> bool:
        """Prüft ob das aktive Backend Embeddings unterstützt.

        Anthropic hat keine Embeddings -- dann wird der Ollama-Fallback genutzt.
        """
        if self._backend_type == "anthropic":
            return False  # Ollama-Fallback wird in embed() automatisch genutzt
        return True
