from __future__ import annotations

"""Minimal LLM abstraction and a deterministic dummy client.

The goal is to provide a stable interface for recipes that *may* use an LLM,
while keeping tests fully deterministic and not depending on any external
service.
"""

from dataclasses import dataclass
from typing import Any, Dict, Protocol


class LLMClient(Protocol):  # pragma: no cover - protocol definition
    """Very small synchronous LLM interface."""

    def generate(self, prompt: str, **kwargs: Any) -> str:
        """Return a completion for ``prompt``.

        Real implementations may ignore ``kwargs`` or use them for decoding
        parameters such as temperature, max tokens, etc.
        """


@dataclass
class DummyLLMClient:
    """Deterministic LLM stub used for tests and local experimentation.

    Behaviour: ``generate(prompt)`` simply returns ``"{prefix}: {prompt}"``.
    """

    prefix: str = "LLM"

    def generate(self, prompt: str, **kwargs: Any) -> str:  # pragma: no cover - trivial
        return f"{self.prefix}: {prompt}"


def build_llm_from_recipe_params(params: Dict[str, Any] | None) -> LLMClient | None:
    """Build an :class:`LLMClient` from ``DatasetRecipe.params``.

    Expected shape (all optional)::

        {
          "llm": {
            "provider": "dummy" | "openai" | "anthropic" | "ollama",

            # For dummy provider:
            "prefix": "LLM-ANS",

            # For openai provider:
            "model": "gpt-4",
            "api_key": "sk-...",  # optional, reads from OPENAI_API_KEY env var
            "temperature": 0.7,
            "max_tokens": 1024,

            # For anthropic provider:
            "model": "claude-3-5-sonnet-20241022",
            "api_key": "sk-ant-...",  # optional, reads from ANTHROPIC_API_KEY env var
            "temperature": 0.7,
            "max_tokens": 1024,

            # For ollama provider:
            "model": "llama3.2",
            "base_url": "http://localhost:11434",
            "temperature": 0.7,
            "max_tokens": 1024,
          }
        }

    If no usable config is present, returns ``None``.
    """

    if not params:
        return None

    cfg = params.get("llm")
    if not isinstance(cfg, dict):
        return None

    provider = str(cfg.get("provider", "dummy")).lower()

    if provider == "dummy":
        prefix = str(cfg.get("prefix", "LLM"))
        return DummyLLMClient(prefix=prefix)

    if provider == "openai":
        from .openai_client import OpenAILLMClient

        return OpenAILLMClient(
            model=str(cfg.get("model", "gpt-3.5-turbo")),
            api_key=cfg.get("api_key"),
            temperature=float(cfg.get("temperature", 0.7)),
            max_tokens=int(cfg.get("max_tokens", 1024)),
        )

    if provider == "anthropic":
        from .anthropic_client import AnthropicLLMClient

        return AnthropicLLMClient(
            model=str(cfg.get("model", "claude-3-5-sonnet-20241022")),
            api_key=cfg.get("api_key"),
            temperature=float(cfg.get("temperature", 0.7)),
            max_tokens=int(cfg.get("max_tokens", 1024)),
        )

    if provider == "ollama":
        from .ollama_client import OllamaLLMClient

        return OllamaLLMClient(
            model=str(cfg.get("model", "llama3.2")),
            base_url=str(cfg.get("base_url", "http://localhost:11434")),
            temperature=float(cfg.get("temperature", 0.7)),
            max_tokens=int(cfg.get("max_tokens", 1024)),
        )

    # Unknown provider: return None so recipes fall back to deterministic behaviour.
    return None

