"""百炼 API 统一封装层。

使用 openai Python SDK（百炼兼容 OpenAI 协议），提供流式和非流式两种调用模式。
所有 AI 应用通过此客户端统一调用阿里云通义千问。
"""

from __future__ import annotations

import asyncio
import copy
import json
import logging
from datetime import datetime
from typing import Any, AsyncGenerator

import openai

logger = logging.getLogger(__name__)


# ── 异常类 ──────────────────────────────────────────────────────────


class BailianApiError(Exception):
    """百炼 API 调用失败（重试耗尽后）。"""

    def __init__(self, message: str, status_code: int | None = None):
        super().__init__(message)
        self.status_code = status_code


class BailianJsonParseError(Exception):
    """百炼 API 返回的 JSON 解析失败。"""

    def __init__(self, message: str, raw_content: str = ""):
        super().__init__(message)
        self.raw_content = raw_content


class BailianAuthError(BailianApiError):
    """百炼 API Key 无效（HTTP 401）。"""

    def __init__(self, message: str = "API Key 无效或已过期"):
        super().__init__(message, status_code=401)


# ── 客户端 ──────────────────────────────────────────────────────────


class BailianClient:
    """百炼 API 统一封装层。

    使用 openai.AsyncOpenAI 客户端，base_url 指向百炼端点。
    提供流式（chat_stream）和非流式（chat_json）两种调用模式。
    """

    # 重试配置
    MAX_RETRIES = 3
    BASE_INTERVAL = 1  # 秒

    def __init__(self, api_key: str, base_url: str, model: str):
        """初始化百炼客户端。

        Args:
            api_key: 百炼 API Key（环境变量 BAILIAN_API_KEY）
            base_url: 百炼 API 端点（环境变量 BAILIAN_BASE_URL）
            model: 模型标识，如 qwen-plus（环境变量 BAILIAN_MODEL）
        """
        self.model = model
        self._client = openai.AsyncOpenAI(
            api_key=api_key,
            base_url=base_url,
        )

    async def chat_stream(
        self,
        messages: list[dict],
        *,
        temperature: float = 0.7,
        max_tokens: int = 2000,
    ) -> AsyncGenerator[str, None]:
        """流式调用，逐 chunk yield 文本。用于应用 1 SSE。

        Args:
            messages: 消息列表
            temperature: 温度参数，默认 0.7
            max_tokens: 最大 token 数，默认 2000

        Yields:
            文本 chunk
        """
        messages = self._inject_current_time(messages)
        response = await self._call_with_retry(
            model=self.model,
            messages=messages,
            temperature=temperature,
            max_tokens=max_tokens,
            stream=True,
        )
        async for chunk in response:
            if chunk.choices and chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content

    async def chat_json(
        self,
        messages: list[dict],
        *,
        temperature: float = 0.3,
        max_tokens: int = 4000,
    ) -> tuple[dict, int]:
        """非流式调用，返回解析后的 JSON dict 和 tokens_used。

        用于应用 2-8 的结构化输出。使用 response_format={"type": "json_object"}
        确保返回合法 JSON。

        Args:
            messages: 消息列表
            temperature: 温度参数，默认 0.3（结构化输出用低温度）
            max_tokens: 最大 token 数，默认 4000

        Returns:
            (parsed_json_dict, tokens_used) 元组

        Raises:
            BailianJsonParseError: 响应内容无法解析为 JSON
            BailianApiError: API 调用失败（重试耗尽后）
        """
        messages = self._inject_current_time(messages)
        response = await self._call_with_retry(
            model=self.model,
            messages=messages,
            temperature=temperature,
            max_tokens=max_tokens,
            stream=False,
            response_format={"type": "json_object"},
        )
        raw_content = response.choices[0].message.content or ""
        tokens_used = response.usage.total_tokens if response.usage else 0

        try:
            parsed = json.loads(raw_content)
        except (json.JSONDecodeError, TypeError) as e:
            logger.error("百炼 API 返回非法 JSON: %s", raw_content[:500])
            raise BailianJsonParseError(
                f"JSON 解析失败: {e}",
                raw_content=raw_content,
            ) from e

        return parsed, tokens_used

    def _inject_current_time(self, messages: list[dict]) -> list[dict]:
        """纯函数：在首条消息的 content（JSON 字符串）中注入 current_time 字段。

        - 深拷贝输入，不修改原始 messages
        - 首条消息 content 尝试解析为 JSON，注入 current_time
        - 如果首条消息 content 不是 JSON，则包装为 JSON
        - 其余消息不变
        - current_time 格式：ISO 8601 精确到秒，如 2026-03-08T14:30:00

        Args:
            messages: 原始消息列表

        Returns:
            注入 current_time 后的新消息列表
        """
        if not messages:
            return []

        result = copy.deepcopy(messages)
        first = result[0]
        content = first.get("content", "")
        now_str = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")

        try:
            parsed = json.loads(content)
            if isinstance(parsed, dict):
                parsed["current_time"] = now_str
            else:
                # content 是合法 JSON 但不是 dict（如数组、字符串），包装为 dict
                parsed = {"original_content": parsed, "current_time": now_str}
        except (json.JSONDecodeError, TypeError):
            # content 不是 JSON，包装为 dict
            parsed = {"content": content, "current_time": now_str}

        first["content"] = json.dumps(parsed, ensure_ascii=False)
        return result

    async def _call_with_retry(self, **kwargs: Any) -> Any:
        """带指数退避的重试封装。

        重试策略：
        - 最多重试 MAX_RETRIES 次（默认 3 次）
        - 间隔：BASE_INTERVAL × 2^(n-1)，即 1s → 2s → 4s
        - HTTP 4xx：不重试，直接抛出（401 → BailianAuthError）
        - HTTP 5xx / 超时：重试

        Args:
            **kwargs: 传递给 openai client 的参数

        Returns:
            API 响应对象

        Raises:
            BailianAuthError: API Key 无效（HTTP 401）
            BailianApiError: API 调用失败（重试耗尽后）
        """
        is_stream = kwargs.get("stream", False)
        last_error: Exception | None = None

        for attempt in range(self.MAX_RETRIES):
            try:
                if is_stream:
                    # 流式调用：返回 async iterator
                    return await self._client.chat.completions.create(**kwargs)
                else:
                    return await self._client.chat.completions.create(**kwargs)

            except openai.AuthenticationError as e:
                # 401：API Key 无效，不重试
                logger.error("百炼 API 认证失败: %s", e)
                raise BailianAuthError(str(e)) from e

            except openai.BadRequestError as e:
                # 400：请求参数错误，不重试
                logger.error("百炼 API 请求参数错误: %s", e)
                raise BailianApiError(str(e), status_code=400) from e

            except openai.RateLimitError as e:
                # 429：限流，不重试（属于 4xx）
                logger.error("百炼 API 限流: %s", e)
                raise BailianApiError(str(e), status_code=429) from e

            except openai.PermissionDeniedError as e:
                # 403：权限不足，不重试
                logger.error("百炼 API 权限不足: %s", e)
                raise BailianApiError(str(e), status_code=403) from e

            except openai.NotFoundError as e:
                # 404：资源不存在，不重试
                logger.error("百炼 API 资源不存在: %s", e)
                raise BailianApiError(str(e), status_code=404) from e

            except openai.UnprocessableEntityError as e:
                # 422：不可处理，不重试
                logger.error("百炼 API 不可处理的请求: %s", e)
                raise BailianApiError(str(e), status_code=422) from e

            except (openai.InternalServerError, openai.APIConnectionError, openai.APITimeoutError) as e:
                # 5xx / 超时 / 连接错误：重试
                last_error = e
                if attempt < self.MAX_RETRIES - 1:
                    wait_time = self.BASE_INTERVAL * (2 ** attempt)
                    logger.warning(
                        "百炼 API 调用失败（第 %d/%d 次），%ds 后重试: %s",
                        attempt + 1,
                        self.MAX_RETRIES,
                        wait_time,
                        e,
                    )
                    await asyncio.sleep(wait_time)
                else:
                    logger.error(
                        "百炼 API 调用失败，已达最大重试次数 %d: %s",
                        self.MAX_RETRIES,
                        e,
                    )

        # 重试耗尽
        status_code = getattr(last_error, "status_code", None)
        raise BailianApiError(
            f"百炼 API 调用失败（重试 {self.MAX_RETRIES} 次后）: {last_error}",
            status_code=status_code,
        ) from last_error
