# retoor <retoor@molodetz.nl>
from __future__ import annotations

import asyncio
import json
import logging
from typing import Any, Optional

import httpx

from devplacepy.stealth import stealth_async_client
from devplacepy.services.jobs.isslop.config import (
    LLM_MAX_RETRIES,
    LLM_RETRY_BACKOFF_SECONDS,
    LLM_TEMPERATURE,
    LLM_TIMEOUT_SECONDS,
    WorkerSettings,
)

logger = logging.getLogger(__name__)


class LlmUnavailableError(Exception):
    pass


class LlmClient:
    def __init__(self, settings: WorkerSettings) -> None:
        self._endpoint = settings.llm_endpoint
        self._model = settings.llm_model
        self._api_key = settings.llm_api_key
        self._review_enabled = settings.ai_review_enabled
        self._vision_enabled = settings.image_review_enabled
        self._client: Optional[httpx.AsyncClient] = None

    @property
    def active_backend_name(self) -> str:
        return self._model

    @property
    def review_available(self) -> bool:
        return self._review_enabled and bool(self._endpoint and self._api_key)

    @property
    def vision_available(self) -> bool:
        return self._vision_enabled and bool(self._endpoint and self._api_key)

    async def _http(self) -> httpx.AsyncClient:
        if self._client is None:
            self._client = stealth_async_client(timeout=httpx.Timeout(LLM_TIMEOUT_SECONDS))
        return self._client

    async def aclose(self) -> None:
        if self._client is not None:
            await self._client.aclose()
            self._client = None

    async def _call(self, messages: list[dict[str, Any]], timeout: Optional[float] = None) -> str:
        if not self._endpoint or not self._api_key:
            raise LlmUnavailableError("AI gateway not configured")
        client = await self._http()
        payload = {
            "model": self._model,
            "messages": messages,
            "temperature": LLM_TEMPERATURE,
        }
        headers = {
            "authorization": f"Bearer {self._api_key}",
            "content-type": "application/json",
            "X-App-Reference": "devplace-isslop-v-1-0-0",
        }
        request_timeout = httpx.Timeout(timeout) if timeout else None
        last_error = "unknown"
        for attempt in range(1, LLM_MAX_RETRIES + 1):
            try:
                response = await client.post(
                    self._endpoint, json=payload, headers=headers, timeout=request_timeout
                )
            except httpx.HTTPError as error:
                last_error = f"{type(error).__name__}: {error}"
                logger.warning("Gateway attempt %d transport failure: %s", attempt, last_error)
                await asyncio.sleep(LLM_RETRY_BACKOFF_SECONDS * attempt)
                continue
            if response.status_code >= 400:
                last_error = f"HTTP {response.status_code}: {response.text[:300]}"
                logger.warning("Gateway attempt %d failed: %s", attempt, last_error)
                await asyncio.sleep(LLM_RETRY_BACKOFF_SECONDS * attempt)
                continue
            try:
                body = response.json()
            except ValueError:
                last_error = "invalid JSON envelope"
                continue
            choices = body.get("choices")
            if isinstance(choices, list) and choices:
                content = choices[0].get("message", {}).get("content")
                if isinstance(content, str) and content.strip():
                    return content
            last_error = json.dumps(body)[:300]
            logger.warning("Gateway attempt %d empty completion: %s", attempt, last_error)
            await asyncio.sleep(LLM_RETRY_BACKOFF_SECONDS * attempt)
        raise LlmUnavailableError(f"AI gateway failed after {LLM_MAX_RETRIES} attempts: {last_error}")

    async def complete(self, system: str, user: str) -> str:
        if not self.review_available:
            raise LlmUnavailableError("AI review disabled")
        return await self._call(
            [
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ]
        )

    async def describe_image(self, prompt: str, image_data_url: str, timeout: float) -> str:
        if not self.vision_available:
            raise LlmUnavailableError("vision backend unavailable")
        return await self._call(
            [
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": prompt},
                        {"type": "image_url", "image_url": {"url": image_data_url}},
                    ],
                }
            ],
            timeout=timeout,
        )


def extract_json_object(text: str) -> Optional[dict[str, Any]]:
    start = text.find("{")
    while start != -1:
        depth = 0
        in_string = False
        escaped = False
        for position in range(start, len(text)):
            char = text[position]
            if in_string:
                if escaped:
                    escaped = False
                elif char == "\\":
                    escaped = True
                elif char == '"':
                    in_string = False
                continue
            if char == '"':
                in_string = True
            elif char == "{":
                depth += 1
            elif char == "}":
                depth -= 1
                if depth == 0:
                    candidate = text[start:position + 1]
                    try:
                        parsed = json.loads(candidate)
                    except json.JSONDecodeError:
                        break
                    if isinstance(parsed, dict):
                        return parsed
                    break
        start = text.find("{", start + 1)
    return None
