# -*- coding: utf-8 -*-
"""
DeepSeek API 客户端封装
支持流式输出、思考模式、工具调用、错误重试
"""
import os
import time
import queue
import threading
from typing import List, Dict, Any, Iterator
from openai import (
    OpenAI,
    APIError,
    APIStatusError,
    RateLimitError,
    APIConnectionError,
    InternalServerError,
)
from . import logger
from .llm.resilience import ApiCircuitBreaker, CircuitOpenError


def _iter_with_idle_timeout(iterator: Iterator[Any], idle_timeout: float) -> Iterator[Any]:
    """包装迭代器，相邻两个 item 间隔超过 idle_timeout 秒则抛 TimeoutError。

    用于检测流式响应在产出部分 chunk 后挂起（服务端或网络问题）的情况：
    主线程通过 queue.get(timeout=...) 消费，daemon 生产者线程驱动原迭代器。

    Args:
        iterator: 原始迭代器
        idle_timeout: 相邻 item 间隔上限（秒）；<=0 或 None 表示不启用

    Yields:
        原迭代器产出的每个 item

    Raises:
        TimeoutError: 相邻 item 间隔超过 idle_timeout
    """
    if not idle_timeout or idle_timeout <= 0:
        yield from iterator
        return

    q: "queue.Queue[Any]" = queue.Queue()
    _SENTINEL = object()

    def producer():
        try:
            for item in iterator:
                q.put(item)
        except BaseException as exc:
            q.put(exc)
        finally:
            q.put(_SENTINEL)

    thread = threading.Thread(target=producer, daemon=True)
    thread.start()

    while True:
        try:
            item = q.get(timeout=idle_timeout)
        except queue.Empty:
            raise TimeoutError(
                f"流式响应 chunk 间隔超过 {idle_timeout}s，疑似服务端挂起"
            )
        if item is _SENTINEL:
            return
        if isinstance(item, BaseException):
            raise item
        yield item


def _extract_cause_summary(exc: BaseException) -> str:
    """从异常链中提取底层 cause 摘要，便于日志诊断。

    OpenAI SDK 的 APIConnectionError 会把 httpx 异常（如 ConnectError /
    ReadTimeout / RemoteProtocolError）作为 __cause__ 链上。原始 str(e)
    只返回 "Connection error."，丢失了真正的失败原因。
    """
    cause = exc.__cause__ or exc.__context__
    if not cause:
        return ""
    cause_type = type(cause).__name__
    cause_msg = str(cause) or ""
    # 截断超长 cause 消息，避免日志膨胀
    if len(cause_msg) > 200:
        cause_msg = cause_msg[:200] + "..."
    if cause_msg:
        return f"{cause_type}: {cause_msg}"
    return cause_type


def _format_retry_reason(exc: BaseException) -> str:
    """构造 retry 日志的 reason 字段，包含异常类型 + cause 摘要。"""
    exc_type = type(exc).__name__
    cause_summary = _extract_cause_summary(exc)
    if cause_summary:
        return f"{exc_type} ({cause_summary})"
    return exc_type


def _log_final_api_failure(model: str, exc: BaseException) -> None:
    """重试耗尽后，记录一条带模型名 + cause 摘要的最终失败日志。

    原 chat() 在重试耗尽时直接 raise，丢失了模型名与 cause 上下文，
    调用方 main.py 只记录了 str(e)（如 "Connection error."）无法定位
    真正原因（DNS / TLS / 服务端 5xx / httpx ReadTimeout 等）。
    """
    exc_type = type(exc).__name__
    cause_summary = _extract_cause_summary(exc)
    parts = [f"model={model}", f"type={exc_type}"]
    if cause_summary:
        parts.append(f"cause={cause_summary}")
    logger.api_error(" | ".join(parts))


class DeepSeekClient:
    """DeepSeek API 客户端封装，支持流式输出、工具调用和错误重试。"""

    # 可重试的错误码
    RETRYABLE_CODES = {429, 500, 503}

    def __init__(self, api_key: str = None, base_url: str = None):
        self.api_key = api_key or os.environ.get("DEEPSEEK_API_KEY")
        self.base_url = base_url or "https://api.deepseek.com"
        self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
        self.max_retries = 3
        self.retry_delay = 1.0
        # API 断路器（Phase 6 AN）：连续失败 5 次后 OPEN，60s 后 HALF_OPEN 探测
        self._breaker = ApiCircuitBreaker(failure_threshold=5, recovery_timeout=60.0)

    def chat(
        self,
        messages: List[Dict[str, Any]],
        model: str = "deepseek-v4-pro",
        tools: List[Dict] = None,
        stream: bool = True,
        thinking: bool = True,
        reasoning_effort: str = "high",
        max_tokens: int = None,
        user_id: str = None,
        stream_idle_timeout: float = None,
        on_413_callback=None,
    ) -> Iterator[Dict]:
        """
        发送对话请求，返回迭代器。

        Args:
            messages: 对话消息列表
            model: 模型名称
            tools: 工具 schema 列表
            stream: 是否流式输出
            thinking: 是否启用思考模式
            reasoning_effort: 推理强度 high/max
            max_tokens: 最大生成 token 数
            user_id: 自定义用户ID，用于隔离
            stream_idle_timeout: 流式响应相邻 chunk 间隔上限（秒）；
                超时抛 TimeoutError，避免服务端挂起时无限阻塞。None/0 表示不启用
            on_413_callback: 可选的 413 两阶段恢复回调，签名为
                ``on_413_callback(phase: int) -> bool``。当 API 返回 HTTP 413
                (Payload Too Large) 时依次以 phase=1、phase=2 调用，返回 True
                表示压缩成功可重试，False 表示压缩失败。两阶段后仍 413 则抛
                RuntimeError("413 两阶段恢复失败")。为 None 时直接抛 413 异常
                （旧行为兼容）。
        """
        extra_body = {"thinking": {"type": "enabled" if thinking else "disabled"}}
        if user_id:
            extra_body["user_id"] = user_id

        kwargs = {
            "model": model,
            "messages": messages,
            "stream": stream,
            "reasoning_effort": reasoning_effort,
            "extra_body": extra_body,
        }
        if tools:
            kwargs["tools"] = tools
        if max_tokens:
            kwargs["max_tokens"] = max_tokens

        logger.api_request(model=model, msgs=len(messages),
                           tools=len(tools) if tools else 0,
                           thinking=thinking)

        # 断路器 fast-fail（Phase 6 AN）：OPEN 态直接拒绝，避免用户长时间等待
        if not self._breaker.allow_request():
            raise CircuitOpenError()

        # 413 两阶段恢复已触发的 phase 计数（0=未触发，1/2=Phase 1/2 已调用）。
        # 与 max_retries 解耦：413 重试独立于 429/500/503 的重试配额。
        _413_phase = 0

        for attempt in range(self.max_retries):
            try:
                response = self.client.chat.completions.create(**kwargs)
                if stream:
                    for chunk in _iter_with_idle_timeout(response, stream_idle_timeout):
                        yield self._parse_chunk(chunk)
                else:
                    yield self._parse_response(response)
                # 413 恢复成功：若曾触发过压缩，记一条成功日志
                if _413_phase > 0:
                    logger.api_retry(_413_phase, 0, f"413 恢复成功（Phase {_413_phase}）")
                # 断路器：成功调用重置失败计数
                self._breaker.record_success()
                return
            except RateLimitError as e:
                if attempt < self.max_retries - 1:
                    wait = self.retry_delay * (2 ** attempt)
                    logger.api_retry(attempt + 1, wait, "rate_limit")
                    time.sleep(wait)
                    continue
                _log_final_api_failure(model, e)
                raise e
            except (APIConnectionError, InternalServerError) as e:
                if attempt < self.max_retries - 1:
                    wait = self.retry_delay * (2 ** attempt)
                    logger.api_retry(attempt + 1, wait, _format_retry_reason(e))
                    time.sleep(wait)
                    continue
                _log_final_api_failure(model, e)
                raise e
            except APIStatusError as e:
                # 413 两阶段恢复：APIStatusError 是 APIError 的子类，必须放在
                # APIError 之前捕获，避免 413 被通用 APIError 分支误判为不可重试。
                if e.status_code == 413:
                    _413_phase += 1
                    if on_413_callback is None:
                        _log_final_api_failure(model, e)
                        raise
                    if _413_phase > 2:
                        _log_final_api_failure(model, e)
                        raise RuntimeError("413 两阶段恢复失败")
                    ok = on_413_callback(_413_phase)
                    if not ok:
                        _log_final_api_failure(model, e)
                        raise RuntimeError(f"413 压缩失败（Phase {_413_phase}）")
                    logger.api_retry(_413_phase, 0, f"413 触发 Phase {_413_phase} 压缩，重试")
                    continue
                # 非 413 的 APIStatusError，落 RETRYABLE_CODES 逻辑
                if e.status_code in self.RETRYABLE_CODES and attempt < self.max_retries - 1:
                    wait = self.retry_delay * (2 ** attempt)
                    logger.api_retry(attempt + 1, wait, f"http_{e.status_code}")
                    time.sleep(wait)
                    continue
                _log_final_api_failure(model, e)
                raise e
            except APIError as e:
                if e.status_code in self.RETRYABLE_CODES and attempt < self.max_retries - 1:
                    wait = self.retry_delay * (2 ** attempt)
                    logger.api_retry(attempt + 1, wait, f"http_{e.status_code}")
                    time.sleep(wait)
                    continue
                _log_final_api_failure(model, e)
                raise e

        # 循环耗尽仍未成功（含 413 重试次数超 max_retries 的边界兜底）
        if _413_phase > 0:
            raise RuntimeError("413 两阶段恢复失败")

    def _parse_chunk(self, chunk) -> Dict[str, Any]:
        """解析流式 chunk"""
        delta = chunk.choices[0].delta if chunk.choices else None
        return {
            "type": "chunk",
            "content": getattr(delta, "content", None),
            "reasoning_content": getattr(delta, "reasoning_content", None),
            "tool_calls": getattr(delta, "tool_calls", None),
            "finish_reason": chunk.choices[0].finish_reason if chunk.choices else None,
            "usage": getattr(chunk, "usage", None)
        }

    def _parse_response(self, response) -> Dict[str, Any]:
        """解析非流式响应"""
        msg = response.choices[0].message
        return {
            "type": "complete",
            "content": getattr(msg, "content", None),
            "reasoning_content": getattr(msg, "reasoning_content", None),
            "tool_calls": getattr(msg, "tool_calls", None),
            "finish_reason": response.choices[0].finish_reason,
            "usage": getattr(response, "usage", None)
        }