"""Adapter Ollama — implémentation concrète des ports LLMProvider et LLMChatProvider.

Isole le reste de l'application des spécificités du protocole Ollama
(URL /api/generate, /api/chat, payload, parsing). Pour swap vers OpenAI
demain, on écrit un nouvel adapter sans toucher au reste du code.
"""
import json
from typing import AsyncIterator

import httpx

from app.core.config import Settings
from app.domain.models import ChatMessage
from app.domain.ports import LLMProviderError


class OllamaLLMProvider:
    """Implémentation des ports LLM — appelle un serveur Ollama via HTTP.

    Satisfait implicitement (duck typing) à la fois `LLMProvider` (endpoint
    /api/generate, appel unique) et `LLMChatProvider` (endpoint /api/chat,
    streaming token par token).
    """

    def __init__(self, settings: Settings) -> None:
        self._base_url = settings.ollama_base_url
        self._model = settings.llm_model
        self._timeout = settings.llm_timeout_seconds

    async def generate(
        self,
        prompt: str,
        *,
        output_format: str | None = None,
        temperature: float | None = None,
    ) -> str:
        url = f"{self._base_url}/api/generate"
        payload: dict[str, object] = {
            "model": self._model,
            "prompt": prompt,
            "stream": False,
        }
        if output_format is not None:
            payload["format"] = output_format
        if temperature is not None:
            # Ollama attend les hyperparamètres sous la clé "options".
            payload["options"] = {"temperature": temperature}

        async with httpx.AsyncClient(timeout=self._timeout) as client:
            try:
                response = await client.post(url, json=payload)
                response.raise_for_status()
            except httpx.HTTPError as exc:
                raise LLMProviderError(
                    f"Erreur lors de l'appel à Ollama : {exc}"
                ) from exc

        return response.json()["response"]

    async def stream_chat(
        self,
        messages: list[ChatMessage],
        *,
        system_prompt: str | None = None,
        temperature: float | None = None,
    ) -> AsyncIterator[str]:
        """Streame depuis Ollama /api/chat. Parse le NDJSON ligne par ligne.

        Ollama renvoie un JSON par ligne au fil de la génération :
          - étapes intermédiaires : `{"message": {"content": "token"}, "done": false}`
          - étape finale           : `{"done": true, ...}`

        On yield chaque token non-vide au consommateur, qui se charge du
        formatage SSE (c'est la responsabilité du controller HTTP, pas
        de l'adapter LLM).
        """
        url = f"{self._base_url}/api/chat"

        payload_messages: list[dict[str, str]] = []
        if system_prompt:
            payload_messages.append({"role": "system", "content": system_prompt})
        payload_messages.extend(
            {"role": m.role, "content": m.content} for m in messages
        )

        payload: dict[str, object] = {
            "model": self._model,
            "messages": payload_messages,
            "stream": True,
        }
        if temperature is not None:
            payload["options"] = {"temperature": temperature}

        async with httpx.AsyncClient(timeout=self._timeout) as client:
            try:
                async with client.stream("POST", url, json=payload) as response:
                    response.raise_for_status()
                    async for line in response.aiter_lines():
                        if not line.strip():
                            continue
                        chunk = json.loads(line)
                        if chunk.get("done"):
                            break
                        token = chunk.get("message", {}).get("content", "")
                        if token:
                            yield token
            except httpx.HTTPError as exc:
                raise LLMProviderError(
                    f"Erreur lors du streaming Ollama : {exc}"
                ) from exc
