"""Synchronous streaming client for MiniMax 2.5 via OpenRouter.

Runs entirely inside the agent's background thread (@work(thread=True)).
Uses httpx.Client (SYNC, not async) so the main Textual event loop is
never touched by network I/O.

Parses SSE (Server-Sent Events) line-by-line, yielding parsed JSON
chunks that the AgentBrain consumes for text streaming and tool-call
delta accumulation.

Includes retry logic: on 5xx / timeout / connection error, retries up
to AGENT_MAX_RETRIES times with exponential backoff (2s, 4s, 8s).
"""

from __future__ import annotations

import json
import logging
import time
from typing import Generator

import httpx

from ..constants import (
    AGENT_HTTP_TIMEOUT,
    AGENT_MAX_RETRIES,
    OPENROUTER_BASE,
    OPENROUTER_MODEL,
)

logger = logging.getLogger(__name__)


class OpenRouterClient:
    """Sync streaming HTTP client for OpenRouter's chat completions API."""

    def __init__(self, api_key: str, model: str = OPENROUTER_MODEL) -> None:
        self.api_key = api_key
        self.model = model
        self.client = httpx.Client(timeout=AGENT_HTTP_TIMEOUT)

    def stream_chat(
        self,
        messages: list[dict],
        tools: list[dict] | None = None,
    ) -> Generator[dict, None, None]:
        """Yield parsed SSE chunks from OpenRouter.

        Each yielded dict is the parsed JSON of one ``data:`` line
        from the SSE stream. The caller accumulates text content
        and tool_call deltas from these chunks.

        Retries on 5xx, timeout, and connection errors up to
        AGENT_MAX_RETRIES times with exponential backoff.

        Raises httpx.HTTPStatusError on non-retryable errors (4xx).
        """
        payload: dict = {
            "model": self.model,
            "messages": messages,
            "stream": True,
        }
        if tools:
            payload["tools"] = tools
            payload["tool_choice"] = "auto"

        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
            "HTTP-Referer": "https://easyclaw.help",
            "X-Title": "EasyClaw Installer",
        }

        last_error: Exception | None = None

        for attempt in range(1, AGENT_MAX_RETRIES + 1):
            try:
                yield from self._stream_once(payload, headers)
                return  # success — done
            except httpx.HTTPStatusError as exc:
                if exc.response.status_code < 500:
                    raise  # 4xx = client error, don't retry
                last_error = exc
                logger.warning(
                    "OpenRouter 5xx (attempt %d/%d): %s",
                    attempt, AGENT_MAX_RETRIES, exc,
                )
            except (httpx.TimeoutException, httpx.ConnectError) as exc:
                last_error = exc
                logger.warning(
                    "OpenRouter %s (attempt %d/%d): %s",
                    type(exc).__name__, attempt, AGENT_MAX_RETRIES, exc,
                )

            if attempt < AGENT_MAX_RETRIES:
                backoff = 2 ** attempt  # 2s, 4s, 8s
                logger.info("Retrying in %ds...", backoff)
                time.sleep(backoff)

        # All retries exhausted
        raise last_error  # type: ignore[misc]

    def _stream_once(
        self, payload: dict, headers: dict,
    ) -> Generator[dict, None, None]:
        """Execute a single streaming request and yield parsed chunks."""
        with self.client.stream(
            "POST", OPENROUTER_BASE, json=payload, headers=headers
        ) as response:
            response.raise_for_status()

            for line in response.iter_lines():
                if not line or line.startswith(":"):
                    continue  # skip empty lines and SSE comments

                if line.startswith("data: "):
                    data = line[6:]
                    if data.strip() == "[DONE]":
                        return
                    try:
                        yield json.loads(data)
                    except json.JSONDecodeError:
                        continue  # skip malformed chunks

    def close(self) -> None:
        """Close the underlying HTTP client."""
        self.client.close()
