"""Ollama HTTP client.

We deliberately avoid the ``ollama`` Python package — the wire format is a
single ``POST /api/chat`` with ``format="json"`` and a system + user message,
so a small ``httpx`` wrapper is enough. This keeps the runtime dependency
footprint smaller and makes the mock-based unit tests trivial.
"""

from __future__ import annotations

from typing import TypeVar

import httpx
from pydantic import BaseModel, ValidationError

from ocr_sprint.config import get_settings
from ocr_sprint.utils.logging import get_logger

_logger = get_logger(__name__)

T = TypeVar("T", bound=BaseModel)


class LLMUnavailableError(RuntimeError):
    """Raised when the Ollama server is unreachable, times out, or returns
    a malformed payload. The pipeline catches this and falls back to the
    regex-only result with a ``llm_fallback`` review flag.
    """


class OllamaClient:
    """Tiny synchronous HTTP wrapper around the Ollama ``/api/chat`` endpoint.

    Parameters
    ----------
    base_url:
        Ollama server URL, e.g. ``http://localhost:11434``.
    model:
        Model tag to invoke (default ``qwen2.5:1.5b`` — chosen for CPU
        latency at acceptable accuracy).
    timeout_s:
        Hard wall-clock timeout for a single request.
    """

    def __init__(
        self,
        base_url: str | None = None,
        model: str | None = None,
        timeout_s: int | None = None,
    ) -> None:
        s = get_settings()
        self.base_url = (base_url or s.llm_base_url).rstrip("/")
        self.model = model or s.llm_model
        self.timeout_s = timeout_s if timeout_s is not None else s.llm_timeout_s

    # ---------- public API ----------

    def chat_json(self, system: str, user: str, schema_cls: type[T]) -> T:
        """Run a single chat completion in JSON mode and validate the
        response against ``schema_cls``. Raises ``LLMUnavailableError`` on
        any transport / parse / validation failure so callers only have one
        exception to handle.
        """
        payload = {
            "model": self.model,
            "stream": False,
            "format": "json",
            "messages": [
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
            # Keep determinism reasonable — we want extraction, not creativity.
            "options": {"temperature": 0.0, "num_ctx": 4096},
        }
        url = f"{self.base_url}/api/chat"

        try:
            with httpx.Client(timeout=self.timeout_s) as client:
                response = client.post(url, json=payload)
                response.raise_for_status()
                data = response.json()
        except (httpx.HTTPError, ValueError) as exc:
            _logger.warning("llm.transport_error", url=url, error=str(exc))
            raise LLMUnavailableError(f"Ollama request failed: {exc}") from exc

        # Ollama returns {"message": {"role": "assistant", "content": "<json>"}}.
        try:
            content = data["message"]["content"]
        except (KeyError, TypeError) as exc:
            _logger.warning("llm.bad_envelope", payload=data)
            raise LLMUnavailableError(f"Ollama response missing message.content: {data!r}") from exc

        try:
            return schema_cls.model_validate_json(content)
        except ValidationError as exc:
            _logger.warning("llm.validation_error", error=str(exc), content=content[:400])
            raise LLMUnavailableError(f"LLM JSON failed schema: {exc}") from exc
