"""VLM client for vision-language scene analysis.

Sends a JPEG frame plus a text prompt to the OpenAI-compatible vision endpoint
(modelapi.klass.dev) and returns the model's text response.  Follows the same
synchronous ``requests``-with-urllib-fallback pattern as ``LLMClient._post``.
"""

from __future__ import annotations

import base64
import json
import logging
import os
from dataclasses import dataclass, field
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)


@dataclass
class VLMConfig:
    """Configuration for the VLM client.

    Attributes:
        endpoint: Full URL for the chat completions endpoint.
        model: Vision model identifier (e.g. the Qwen vision variant on modelapi).
        api_key: Bearer token for Authorization header.  Reads from
            ``MODELAPI_KEY`` env var if not supplied explicitly.
        timeout_s: Request timeout in seconds.
    """

    endpoint: str = field(
        default_factory=lambda: os.getenv(
            "VLM_ENDPOINT",
            "https://modelapi.klass.dev/v1/chat/completions",
        )
    )
    model: str = field(
        default_factory=lambda: os.getenv("VLM_MODEL", "Qwen3.5-122B-A10B")
    )
    api_key: Optional[str] = field(default=None)
    timeout_s: int = 30

    def __post_init__(self) -> None:
        if self.api_key is None:
            self.api_key = (
                os.getenv("MODELAPI_KEY")
                or os.getenv("MODEL_API_KEY")
                or os.getenv("OPENAI_API_KEY")
            )


class VLMClient:
    """Synchronous HTTP client for the vision-language model endpoint.

    Usage::

        client = VLMClient()
        analysis = client.analyze_scene(jpeg_bytes, "Describe the scene.")
        # "There are two unconscious workers near a chemical container..."

    The endpoint accepts an OpenAI-compatible chat completion request with an
    image embedded as a base64 data URL.  This is a *synchronous* method so
    it can be offloaded to a thread pool from async code via
    ``loop.run_in_executor(None, client.analyze_scene, frame, prompt)``.
    """

    def __init__(self, config: Optional[VLMConfig] = None) -> None:
        self.config = config or VLMConfig()

    def analyze_scene(self, jpeg_bytes: bytes, prompt: str) -> str:
        """Send a JPEG frame and a text prompt to the VLM; return the reply.

        Args:
            jpeg_bytes: Raw JPEG bytes of the frame to analyse.
            prompt: Natural-language instruction for the model.

        Returns:
            The model's text response, or an empty string on any error.
        """
        if not jpeg_bytes:
            logger.warning("VLMClient.analyze_scene called with empty bytes — skipping")
            return ""

        if not prompt:
            logger.warning("VLMClient.analyze_scene called with empty prompt — skipping")
            return ""

        b64 = base64.b64encode(jpeg_bytes).decode("ascii")

        payload: Dict[str, Any] = {
            "model": self.config.model,
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {
                            "type": "image_url",
                            "image_url": {"url": f"data:image/jpeg;base64,{b64}"},
                        },
                        {"type": "text", "text": prompt},
                    ],
                }
            ],
            "temperature": 0.7,
            "max_tokens": 1024,
            "top_p": 0.8,
            "top_k": 20,
        }

        try:
            response = self._post(payload)
            content = response["choices"][0]["message"]["content"]
            if isinstance(content, str):
                logger.info("VLM response: %s…", content[:120])
                return content
            logger.error("VLM response content is not a string: %r", content)
            return ""
        except (KeyError, IndexError, TypeError) as exc:
            logger.error("Unexpected VLM response format: %s", exc)
            return ""
        except Exception as exc:
            logger.error("VLM analyze_scene failed: %s", exc)
            return ""

    # ------------------------------------------------------------------
    # Internal HTTP helpers — mirrors LLMClient._post exactly
    # ------------------------------------------------------------------

    def _post(self, payload: Dict[str, Any]) -> Dict[str, Any]:
        """POST JSON payload to the VLM endpoint.

        Tries ``requests`` first (faster, richer error messages); falls back to
        stdlib ``urllib`` so the client works without the optional dependency.

        Args:
            payload: Dict to serialise as JSON request body.

        Returns:
            Parsed JSON response dict.

        Raises:
            RuntimeError: On any HTTP or network error.
        """
        headers = {"Content-Type": "application/json"}
        if self.config.api_key:
            headers["Authorization"] = f"Bearer {self.config.api_key}"

        data = json.dumps(payload).encode("utf-8")

        try:
            import requests

            response = requests.post(
                self.config.endpoint,
                json=payload,
                headers=headers,
                timeout=self.config.timeout_s,
            )
            response.raise_for_status()
            return response.json()
        except ImportError:
            pass
        except Exception as exc:
            raise RuntimeError(f"VLM request failed: {exc}") from exc

        # urllib fallback
        import urllib.error
        import urllib.request

        request = urllib.request.Request(
            self.config.endpoint,
            data=data,
            headers=headers,
            method="POST",
        )
        try:
            with urllib.request.urlopen(request, timeout=self.config.timeout_s) as handle:
                body = handle.read().decode("utf-8")
                return json.loads(body)
        except urllib.error.HTTPError as exc:
            detail = exc.read().decode("utf-8", errors="replace")
            raise RuntimeError(f"VLM HTTP error {exc.code}: {detail}") from exc
        except Exception as exc:
            raise RuntimeError(f"VLM request failed: {exc}") from exc
