from collections.abc import AsyncIterator

import httpx

from app.providers.llm.base import (
    LLMProvider,
    lang_instruction,
    with_lang_reminder,
    sse_delta,
)


class LocalOpenAICompatibleLLM(LLMProvider):
    def __init__(
        self,
        base_url: str,
        api_key: str,
        model: str,
        system_prompt: str = "",
        disable_reasoning: bool = True,
        max_tokens: int = 0,
        temperature: float = 0.3,
    ):
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self.model = model
        self.system_prompt = (system_prompt or "").strip()
        self.disable_reasoning = disable_reasoning
        self.max_tokens = max_tokens
        self.temperature = temperature

    def _build_messages(
        self, text: str, history: list[dict] | None, language: str | None = None
    ) -> list[dict]:
        # Manche Chat-Templates (z. B. Qwen3) erlauben nur EINE System-Nachricht,
        # ganz am Anfang. Daher den Sprach-System-Prompt und etwaige System-
        # Nachrichten aus der History (z. B. Nutzer-Erinnerungen) zu einer einzigen
        # fuehrenden System-Nachricht zusammenfuehren.
        system_parts: list[str] = []
        if self.system_prompt:
            system_parts.append(self.system_prompt)
        rest: list[dict] = []
        for msg in history or []:
            if msg.get("role") == "system":
                content = (msg.get("content") or "").strip()
                if content:
                    system_parts.append(content)
            else:
                rest.append(msg)
        instr = lang_instruction(language)
        if instr:
            system_parts.append(instr)

        messages: list[dict] = []
        if system_parts:
            messages.append({"role": "system", "content": "\n\n".join(system_parts)})
        messages.extend(rest)
        # Sprach-Erinnerung direkt an der letzten Nutzer-Nachricht (schlägt History-Trägheit).
        messages.append({"role": "user", "content": with_lang_reminder(text, language)})
        return messages

    def _payload(
        self, text: str, history: list[dict] | None, stream: bool, language: str | None = None
    ) -> dict:
        payload: dict = {
            "model": self.model,
            "messages": self._build_messages(text, history, language=language),
            "temperature": self.temperature,
        }
        if stream:
            payload["stream"] = True
        if self.max_tokens > 0:
            payload["max_tokens"] = self.max_tokens
        if self.disable_reasoning:
            # Qwen3/llama.cpp: Denkphase abschalten -> schnellere erste Antwort.
            payload["chat_template_kwargs"] = {"enable_thinking": False}
        return payload

    async def complete(
        self,
        text: str,
        history: list[dict] | None = None,
        session_id: str | None = None,
        language: str | None = None,
    ) -> str:
        async with httpx.AsyncClient(timeout=120) as client:
            response = await client.post(
                f"{self.base_url}/chat/completions",
                headers={"Authorization": f"Bearer {self.api_key}"},
                json=self._payload(text, history, stream=False, language=language),
            )
            response.raise_for_status()
            data = response.json()
            return data["choices"][0]["message"]["content"]

    async def stream(
        self,
        text: str,
        history: list[dict] | None = None,
        session_id: str | None = None,
        language: str | None = None,
    ) -> AsyncIterator[str]:
        async with httpx.AsyncClient(timeout=120) as client:
            async with client.stream(
                "POST",
                f"{self.base_url}/chat/completions",
                headers={"Authorization": f"Bearer {self.api_key}"},
                json=self._payload(text, history, stream=True, language=language),
            ) as response:
                if response.status_code >= 400:
                    body = await response.aread()
                    raise RuntimeError(
                        f"Local LLM error {response.status_code}: "
                        f"{body.decode(errors='replace')}"
                    )
                async for line in response.aiter_lines():
                    delta = sse_delta(line)
                    if delta:
                        yield delta
