"""OpenAI Provider 适配器.

基于 httpx 异步调用 OpenAI 兼容 REST API（/chat/completions）。
不依赖 openai SDK，纯 httpx 实现，便于控制超时与重试。
"""

import json
from collections.abc import AsyncGenerator
from typing import Any

import httpx
import structlog

from ..errors import AILLMUnavailableError
from .base import LLMProvider, LLMResponse, LLMStreamChunk

logger = structlog.get_logger()


class OpenAIProvider(LLMProvider):
    """OpenAI 兼容 API Provider.

    支持所有 OpenAI 兼容端点（OpenAI 官方 / Azure OpenAI / 自部署 vLLM 等）。
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = "https://api.openai.com/v1",
        timeout: float = 30.0,
        stream_connect_timeout: float = 30.0,
        stream_read_timeout: float = 60.0,
    ) -> None:
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._stream_connect_timeout = stream_connect_timeout
        self._stream_read_timeout = stream_read_timeout

    @property
    def name(self) -> str:
        return "openai"

    def is_available(self) -> bool:
        return bool(self._api_key)

    def _build_url(self) -> str:
        return f"{self._base_url}/chat/completions"

    def _build_headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self._api_key}",
            "Content-Type": "application/json",
        }

    async def chat(
        self,
        messages: list[dict[str, str]],
        model: str,
        temperature: float = 0.7,
        **kwargs: Any,
    ) -> LLMResponse:
        if not self.is_available():
            raise AILLMUnavailableError("openai api_key not configured")

        payload: dict[str, Any] = {
            "model": model,
            "messages": messages,
            "temperature": temperature,
            "stream": False,
        }
        payload.update(kwargs)

        try:
            async with httpx.AsyncClient(timeout=self._timeout) as client:
                resp = await client.post(
                    self._build_url(),
                    json=payload,
                    headers=self._build_headers(),
                )
                resp.raise_for_status()
                data = resp.json()
        except httpx.HTTPStatusError as exc:
            logger.error(
                "openai_chat_http_error",
                status_code=exc.response.status_code,
                body=exc.response.text[:500],
            )
            raise AILLMUnavailableError(
                f"openai http {exc.response.status_code}: {exc.response.text[:200]}",
            ) from exc
        except httpx.HTTPError as exc:
            logger.error("openai_chat_http_error", error=str(exc))
            raise AILLMUnavailableError(f"openai http error: {exc}") from exc

        choices = data.get("choices", [])
        content = ""
        if choices:
            content = choices[0].get("message", {}).get("content", "") or ""
        usage_raw = data.get("usage", {}) or {}
        usage = {
            "prompt_tokens": int(usage_raw.get("prompt_tokens", 0)),
            "completion_tokens": int(usage_raw.get("completion_tokens", 0)),
            "total_tokens": int(usage_raw.get("total_tokens", 0)),
        }
        return LLMResponse(
            content=content,
            model=data.get("model", model),
            usage=usage,
            provider=self.name,
            raw=data,
        )

    async def stream_chat(
        self,
        messages: list[dict[str, str]],
        model: str,
        temperature: float = 0.7,
        **kwargs: Any,
    ) -> AsyncGenerator[LLMStreamChunk, None]:
        if not self.is_available():
            raise AILLMUnavailableError("openai api_key not configured")

        payload: dict[str, Any] = {
            "model": model,
            "messages": messages,
            "temperature": temperature,
            "stream": True,
        }
        payload.update(kwargs)

        timeout = httpx.Timeout(
            connect=self._stream_connect_timeout,
            read=self._stream_read_timeout,
            write=self._stream_connect_timeout,
            pool=self._stream_connect_timeout,
        )

        try:
            async with (
                httpx.AsyncClient(timeout=timeout) as client,
                client.stream(
                    "POST",
                    self._build_url(),
                    json=payload,
                    headers=self._build_headers(),
                ) as resp,
            ):
                resp.raise_for_status()
                async for line in resp.aiter_lines():
                    chunk = self._parse_sse_line(line, model)
                    if chunk is not None:
                        yield chunk
                        if chunk.finish_reason == "stop":
                            return
        except httpx.HTTPStatusError as exc:
            logger.error(
                "openai_stream_http_error",
                status_code=exc.response.status_code,
            )
            raise AILLMUnavailableError(
                f"openai stream http {exc.response.status_code}",
            ) from exc
        except httpx.HTTPError as exc:
            logger.error("openai_stream_http_error", error=str(exc))
            raise AILLMUnavailableError(f"openai stream error: {exc}") from exc

    @staticmethod
    def _parse_sse_line(line: str, model: str) -> LLMStreamChunk | None:
        """解析 OpenAI SSE 格式单行.

        Returns:
            LLMStreamChunk 或 None（空行/非 data 行/[DONE] 尾标记）。
        """
        if not line or not line.startswith("data: "):
            return None
        data_str = line[len("data: "):]
        if data_str.strip() == "[DONE]":
            return LLMStreamChunk(delta="", model=model, finish_reason="stop")
        try:
            data = json.loads(data_str)
        except json.JSONDecodeError:
            return None
        choices = data.get("choices", [])
        if not choices:
            return None
        delta = choices[0].get("delta", {}).get("content", "") or ""
        finish_reason = choices[0].get("finish_reason")
        return LLMStreamChunk(
            delta=delta,
            model=data.get("model", model),
            finish_reason=finish_reason,
            provider="openai",
        )

    async def embed(self, text: str, model: str = "text-embedding-3-small") -> list[float]:
        """文本向量化（OpenAI embedding API）."""
        if not self.is_available():
            raise AILLMUnavailableError("openai api_key not configured")
        url = f"{self._base_url}/embeddings"
        try:
            async with httpx.AsyncClient(timeout=self._timeout) as client:
                resp = await client.post(
                    url,
                    json={"model": model, "input": text},
                    headers=self._build_headers(),
                )
                resp.raise_for_status()
                data = resp.json()
        except httpx.HTTPError as exc:
            logger.error("openai_embed_error", error=str(exc))
            raise AILLMUnavailableError(f"openai embed error: {exc}") from exc
        embeddings = data.get("data", [])
        if not embeddings:
            return []
        return embeddings[0].get("embedding", [])
