"""OpenAI 兼容适配器：一套覆盖 DeepSeek/Kimi/Qwen/GLM/OpenAI（ARCH §4.2）。

仅 base_url + model + key 不同。注入 `AsyncOpenAI` 客户端以便测试用替身。
"""

from __future__ import annotations

from collections.abc import AsyncIterator
from typing import Any, Protocol

import instructor
from openai import AsyncOpenAI
from openai.types.chat import ChatCompletionMessageParam
from pydantic import BaseModel

from ..errors import TransientProviderError
from ..types import LlmRequest
from .base import (
    Capabilities,
    ProviderResult,
    ProviderUsage,
    StreamChunk,
    is_transient_by_name,
)

# OpenAI 兼容 SDK 的瞬时错误类名（按名匹配，覆盖 DeepSeek/Kimi/Qwen/GLM 等共用 SDK）。
_TRANSIENT_NAMES = frozenset(
    {
        "RateLimitError",
        "APITimeoutError",
        "APIConnectionError",
        "InternalServerError",
        "APIStatusError",
    }
)


def _is_transient(exc: Exception) -> bool:
    return is_transient_by_name(exc, _TRANSIENT_NAMES)


class StructuredClient(Protocol):
    """instructor 风格的结构化客户端缝（`AsyncInstructor` 即满足此协议）。

    抽成 Protocol 以便测试注入 fake，绝不联网（不变量：测试零真实 LLM）。
    """

    async def create_with_completion(
        self, *, messages: Any, response_model: type[BaseModel], **kwargs: Any
    ) -> tuple[BaseModel, Any]: ...


def _system_text(req: LlmRequest) -> str:
    return "\n\n".join(b.text for b in req.system)


def _input_text(req: LlmRequest) -> str:
    if isinstance(req.input, str):
        return req.input
    return "\n\n".join(b.text for b in req.input)


def _messages(req: LlmRequest) -> list[ChatCompletionMessageParam]:
    msgs: list[ChatCompletionMessageParam] = []
    system = _system_text(req)
    if system:
        msgs.append({"role": "system", "content": system})
    msgs.append({"role": "user", "content": _input_text(req)})
    return msgs


def _cache_read(usage: Any) -> int:
    details = getattr(usage, "prompt_tokens_details", None)
    if details is None:
        return 0
    return int(getattr(details, "cached_tokens", 0) or 0)


def _usage_from(usage: Any) -> ProviderUsage:
    if usage is None:
        return ProviderUsage(input_tokens=0, output_tokens=0)
    return ProviderUsage(
        input_tokens=getattr(usage, "prompt_tokens", 0) or 0,
        output_tokens=getattr(usage, "completion_tokens", 0) or 0,
        cache_read_tokens=_cache_read(usage),
    )


class OpenAICompatAdapter:
    def __init__(
        self,
        provider: str,
        client: AsyncOpenAI,
        *,
        structured_client: StructuredClient | None = None,
    ) -> None:
        self.provider = provider
        self._client = client
        # 结构化输出走 instructor（Pydantic 校验 + 重试，锁定栈）；可注入便于测试。
        self._structured_client = structured_client

    def capabilities(self) -> Capabilities:
        return Capabilities(structured_output=True, prefix_cache=True, thinking=False)

    def _structured(self) -> StructuredClient:
        if self._structured_client is None:
            # 懒构建：从同一 AsyncOpenAI client patch 出 instructor 客户端。
            self._structured_client = instructor.from_openai(self._client)
        return self._structured_client

    async def complete(self, req: LlmRequest, model: str) -> ProviderResult:
        try:
            if req.output_schema is not None:
                return await self._complete_structured(req, model)
            return await self._complete_text(req, model)
        except Exception as exc:
            if _is_transient(exc):
                raise TransientProviderError(str(exc), provider=self.provider) from exc
            raise

    async def _complete_text(self, req: LlmRequest, model: str) -> ProviderResult:
        resp = await self._client.chat.completions.create(
            model=model,
            messages=_messages(req),
            max_tokens=req.max_tokens,
        )
        text = resp.choices[0].message.content or ""
        return ProviderResult(text=text, usage=_usage_from(resp.usage))

    async def _complete_structured(self, req: LlmRequest, model: str) -> ProviderResult:
        if req.output_schema is None:
            raise ValueError("_complete_structured called without output_schema")
        parsed, raw = await self._structured().create_with_completion(
            messages=_messages(req),
            response_model=req.output_schema,
            model=model,
            max_tokens=req.max_tokens,
        )
        usage = _usage_from(getattr(raw, "usage", None))
        # 文本载体保留校验后的 JSON（便于日志/留痕）；程序消费走 parsed。
        return ProviderResult(text=parsed.model_dump_json(), usage=usage, parsed=parsed)

    async def stream(self, req: LlmRequest, model: str) -> AsyncIterator[StreamChunk]:
        try:
            stream = await self._client.chat.completions.create(
                model=model,
                messages=_messages(req),
                max_tokens=req.max_tokens,
                stream=True,
                stream_options={"include_usage": True},
            )
            async for chunk in stream:
                if chunk.choices:
                    delta = chunk.choices[0].delta
                    if delta and delta.content:
                        yield StreamChunk(text=delta.content)
                if getattr(chunk, "usage", None):
                    yield StreamChunk(usage=_usage_from(chunk.usage))
        except Exception as exc:
            if _is_transient(exc):
                raise TransientProviderError(str(exc), provider=self.provider) from exc
            raise
