"""
统一 AI 模型调用客户端（V2 — 云端 API）

设计目标：
1. 消除 httpx 直调 / LangChain / DeerFlow HTTP 三种调用方式并存的问题
2. 统一模型配置获取（DB → .env fallback）
3. 统一 API Key 解密
4. 统一错误处理和超时
5. 支持 chat / embedding / rerank 三种操作

优先级逻辑：
  DB 配置 > .env 默认值(云端)

用法：
    from app.core.llm_client import llm_client

    result = await llm_client.chat(scene_key="chat_model", messages=[...])
    vectors = await llm_client.embed(texts=["hello"])
    ranked = await llm_client.rerank(query="查询", documents=[...])
"""
import httpx
import json
import logging
from typing import Optional, List, Dict, Any, AsyncGenerator

from .config import settings, _decrypt_value

logger = logging.getLogger(__name__)


class LLMClient:
    """统一 AI 模型调用客户端（云端 API）"""

    def __init__(self):
        self._http_client: Optional[httpx.AsyncClient] = None

    async def _get_http_client(self) -> httpx.AsyncClient:
        """获取或创建 httpx 客户端（连接复用）"""
        if self._http_client is None or self._http_client.is_closed:
            self._http_client = httpx.AsyncClient(timeout=60.0)
        return self._http_client

    async def close(self):
        """关闭 httpx 客户端"""
        if self._http_client and not self._http_client.is_closed:
            await self._http_client.aclose()
            self._http_client = None

    # ============ 配置获取 ============

    async def get_scene_config(self, scene_key: str) -> Dict[str, Any]:
        """
        获取场景模型配置（DB 优先 → .env 默认值）

        返回: {
            "provider": "siliconflow" | "openai_compatible",
            "api_base": "...",
            "api_key": "...",
            "model": "...",
            "enabled": True,
        }
        """
        # 1. 尝试从 DB 读取（用户在前端 AI 配置页面保存的配置）
        try:
            from .database import get_db, get_system_db, _has_split_databases
            _db_gen = get_system_db() if _has_split_databases() else get_db()
            async for conn, cur in _db_gen:
                await cur.execute(
                    "SELECT config_value FROM system_configs WHERE config_key = ?",
                    (f"ai_scene_{scene_key}",)
                )
                row = await cur.fetchone()
                if row:
                    val = row["config_value"] if isinstance(row, dict) else row[0]
                    if val:
                        config = json.loads(val)
                        # 关键字段为空时视为未配置，fallback 到 .env
                        if not config.get("model"):
                            logger.debug(f"DB 场景配置({scene_key}) 的 model 为空，回退到 .env")
                            return self._get_env_primary(scene_key)
                        if config.get("api_key"):
                            config["api_key"] = _decrypt_value(config["api_key"])
                        return config
                break
        except Exception as e:
            logger.debug(f"DB 读取场景配置失败({scene_key}): {e}")

        # 2. Fallback 到 .env 默认值（云端优先）
        return self._get_env_primary(scene_key)

    def _get_env_primary(self, scene_key: str) -> Dict[str, Any]:
        """
        从 .env 获取默认配置（云端 SiliconFlow）

        DB 未配置时使用此方法。
        """
        if scene_key == "chat_model":
            return {
                "provider": "siliconflow",
                "api_base": settings.SILICONFLOW_API_BASE,
                "api_key": getattr(settings, "SILICONFLOW_API_KEY", ""),
                "model": getattr(settings, "SF_CHAT_MODEL", "Qwen/Qwen2.5-7B-Instruct"),
                "enabled": True,
            }

        elif scene_key == "vision_model":
            return {
                "provider": getattr(settings, "VISION_PROVIDER", "siliconflow"),
                "api_base": getattr(settings, "VISION_API_BASE", ""),
                "api_key": getattr(settings, "VISION_API_KEY", ""),
                "model": getattr(settings, "VISION_MODEL", ""),
                "enabled": bool(getattr(settings, "VISION_MODEL", "")),
            }

        elif scene_key == "embedding_model":
            return {
                "provider": "siliconflow",
                "api_base": settings.SILICONFLOW_API_BASE,
                "api_key": getattr(settings, "SILICONFLOW_API_KEY", ""),
                "model": "BAAI/bge-m3",
                "enabled": True,
            }

        elif scene_key == "rerank_model":
            return {
                "provider": "siliconflow",
                "api_base": settings.SILICONFLOW_API_BASE,
                "api_key": getattr(settings, "SILICONFLOW_API_KEY", ""),
                "model": "BAAI/bge-reranker-v2-m3",
                "enabled": True,
            }

        else:
            return self._get_env_primary("chat_model")

    # ============ Chat 调用 ============

    async def chat(
        self,
        messages: List[Dict[str, str]],
        scene_key: str = "chat_model",
        model: Optional[str] = None,
        max_tokens: int = 2048,
        temperature: float = 0.7,
        timeout: float = 60.0,
    ) -> Optional[str]:
        """统一 Chat 调用（云端 API）"""
        config = await self.get_scene_config(scene_key)
        if not config.get("enabled", True):
            logger.warning(f"场景 {scene_key} 未启用")
            return None

        api_base = config.get("api_base", "")
        api_key = config.get("api_key", "")
        model_name = model or config.get("model", "")

        if not api_base or not model_name:
            logger.error(f"场景 {scene_key} 配置不完整: api_base={api_base}, model={model_name}")
            return None

        return await self._do_chat(api_base, api_key, model_name, messages, max_tokens, temperature, timeout)

    async def _do_chat(
        self,
        api_base: str,
        api_key: str,
        model: str,
        messages: List[Dict[str, str]],
        max_tokens: int,
        temperature: float,
        timeout: float,
    ) -> Optional[str]:
        """执行 Chat API 调用"""
        url = f"{api_base.rstrip('/')}/chat/completions"
        headers = {"Content-Type": "application/json"}
        if api_key:
            headers["Authorization"] = f"Bearer {api_key}"

        payload = {
            "model": model,
            "messages": messages,
            "max_tokens": max_tokens,
            "temperature": temperature,
        }

        try:
            client = await self._get_http_client()
            resp = await client.post(url, json=payload, headers=headers, timeout=timeout)
            resp.raise_for_status()
            data = resp.json()
            return data["choices"][0]["message"]["content"]
        except httpx.HTTPStatusError as e:
            logger.error(f"Chat API HTTP 错误: {e.response.status_code} - {e.response.text[:200]}")
        except Exception as e:
            logger.debug(f"Chat API 调用失败: {e}")
        return None

    # ============ Embedding 调用 ============

    async def embed(
        self,
        texts: List[str],
        scene_key: str = "embedding_model",
        model: Optional[str] = None,
        timeout: float = 30.0,
    ) -> Optional[List[List[float]]]:
        """统一 Embedding 调用（云端 API）"""
        config = await self.get_scene_config(scene_key)
        api_base = config.get("api_base", "")
        api_key = config.get("api_key", "")
        model_name = model or config.get("model", "")

        if not api_base or not model_name:
            logger.error(f"Embedding 配置不完整: api_base={api_base}, model={model_name}")
            return None

        return await self._do_embed(api_base, api_key, model_name, texts, timeout)

    async def _do_embed(
        self,
        api_base: str,
        api_key: str,
        model: str,
        texts: List[str],
        timeout: float,
    ) -> Optional[List[List[float]]]:
        """执行 Embedding API 调用"""
        url = f"{api_base.rstrip('/')}/embeddings"
        payload = {"model": model, "input": texts, "encoding_format": "float"}

        headers = {"Content-Type": "application/json"}
        if api_key:
            headers["Authorization"] = f"Bearer {api_key}"

        try:
            client = await self._get_http_client()
            resp = await client.post(url, json=payload, headers=headers, timeout=timeout)
            resp.raise_for_status()
            data = resp.json()
            return [item["embedding"] for item in data.get("data", [])]
        except Exception as e:
            logger.debug(f"Embedding API 调用失败: {e}")
        return None

    # ============ Rerank 调用 ============

    async def rerank(
        self,
        query: str,
        documents: List[str],
        scene_key: str = "rerank_model",
        model: Optional[str] = None,
        top_n: int = 5,
        timeout: float = 30.0,
    ) -> Optional[List[Dict[str, Any]]]:
        """统一 Rerank 调用（云端 API）"""
        config = await self.get_scene_config(scene_key)
        api_base = config.get("api_base", "")
        api_key = config.get("api_key", "")
        model_name = model or config.get("model", "")

        if not api_base or not model_name:
            logger.error(f"Rerank 配置不完整: api_base={api_base}, model={model_name}")
            return None

        return await self._do_rerank(api_base, api_key, model_name, query, documents, top_n, timeout)

    async def _do_rerank(
        self,
        api_base: str,
        api_key: str,
        model: str,
        query: str,
        documents: List[str],
        top_n: int,
        timeout: float,
    ) -> Optional[List[Dict[str, Any]]]:
        """执行 Rerank API 调用（OpenAI 兼容 /rerank 端点）"""
        headers = {"Content-Type": "application/json"}
        if api_key:
            headers["Authorization"] = f"Bearer {api_key}"

        url = f"{api_base.rstrip('/')}/rerank"
        payload = {
            "model": model,
            "query": query,
            "documents": documents,
            "top_n": top_n,
            "return_documents": False,
        }
        try:
            client = await self._get_http_client()
            resp = await client.post(url, json=payload, headers=headers, timeout=timeout)
            resp.raise_for_status()
            data = resp.json()
            return data.get("results", [])
        except Exception as e:
            logger.debug(f"Rerank API 调用失败: {e}")
        return None

    # ============ 流式 Chat 调用 ============

    async def chat_stream(
        self,
        messages: List[Dict[str, str]],
        scene_key: str = "chat_model",
        model: Optional[str] = None,
        max_tokens: int = 4096,
        temperature: float = 0.7,
        timeout: float = 120.0,
    ) -> AsyncGenerator[str, None]:
        """流式 Chat 调用（SSE）（云端 API）"""
        config = await self.get_scene_config(scene_key)
        api_base = config.get("api_base", "")
        api_key = config.get("api_key", "")
        model_name = model or config.get("model", "")

        if not api_base or not model_name:
            logger.error(f"流式 Chat 配置不完整: scene={scene_key}")
            return

        async for chunk in self._do_chat_stream(api_base, api_key, model_name, messages, max_tokens, temperature, timeout):
            yield chunk

    async def _do_chat_stream(
        self,
        api_base: str,
        api_key: str,
        model: str,
        messages: List[Dict[str, str]],
        max_tokens: int,
        temperature: float,
        timeout: float,
    ) -> AsyncGenerator[str, None]:
        """执行流式 Chat API 调用"""
        url = f"{api_base.rstrip('/')}/chat/completions"
        headers = {"Content-Type": "application/json"}
        if api_key:
            headers["Authorization"] = f"Bearer {api_key}"

        payload = {
            "model": model,
            "messages": messages,
            "max_tokens": max_tokens,
            "temperature": temperature,
            "stream": True,
        }

        try:
            client = await self._get_http_client()
            async with client.stream("POST", url, json=payload, headers=headers, timeout=timeout) as resp:
                resp.raise_for_status()
                async for line in resp.aiter_lines():
                    if not line.startswith("data: "):
                        continue
                    data_str = line[6:]
                    if data_str.strip() == "[DONE]":
                        break
                    try:
                        data = json.loads(data_str)
                        delta = data.get("choices", [{}])[0].get("delta", {})
                        content = delta.get("content", "")
                        if content:
                            yield content
                    except json.JSONDecodeError:
                        continue
        except Exception as e:
            logger.error(f"流式 Chat 调用失败: {e}")


# 全局单例
llm_client = LLMClient()
