"""
Qwen API 封装（DashScope / 本地 Ollama）。

文本 → 情绪分析 → 标准化 JSON 输出。
"""

import json
import os
import time
from typing import Optional

import requests

# 确保 .env 已加载（无论从哪个路径导入）
import config.settings  # noqa: F401


class QwenClient:
    """
    Qwen 情绪分析客户端。

    支持两种后端：
    1. DashScope API（云）- 需要 QWEN_API_KEY
    2. 本地 Ollama（本地）- 需要 QWEN_LOCAL_BASE_URL

    优先级：本地 > API

    参数:
        api_key: DashScope API Key，默认从环境变量 QWEN_API_KEY 读取
        model: 模型名称，默认从 QWEN_MODEL 读取
        local_base_url: Ollama 地址，默认从 QWEN_LOCAL_BASE_URL 读取
        local_model: Ollama 模型名，默认从 QWEN_LOCAL_MODEL 读取
        max_retries: 失败重试次数
        cache_enabled: 是否启用结果缓存（按文本 hash）
    """

    # 情绪分析 Prompt
    SYSTEM_PROMPT = """你是一个专业的金融情绪分析专家。分析以下 A 股相关的新闻/公告文本，返回 JSON 格式的分析结果。

分析维度：
1. sentiment_score: -1.0(极度利空) ~ 0(中性) ~ +1.0(极度利好)，保留1位小数
2. impact_duration: "short"(1-3个交易日) | "medium"(1-2周) | "long"(1个月以上)
3. confidence: 0.0~1.0 置信度
4. key_topics: 涉及的关键主题列表（最多5个）
5. affected_factors: 可能影响的基本面/技术面因子类型列表

重要规则：
- 只考虑对股价的直接影响
- 中性新闻（常规报道、例行公告）给 0 分
- 重大利好（业绩超预期、政策支持、大订单）给 >0.5 分
- 重大利空（亏损、处罚、减持、诉讼）给 <-0.5 分
- 如果不确定，confidences 要低 (<0.5)

返回格式（仅 JSON）：
{"sentiment_score": 0.0, "impact_duration": "short", "confidence": 0.5, "key_topics": [], "affected_factors": []}"""

    def __init__(
        self,
        api_key: str | None = None,
        model: str | None = None,
        local_base_url: str | None = None,
        local_model: str | None = None,
        max_retries: int = 3,
        cache_enabled: bool = True,
    ):
        self.api_key = api_key or os.getenv("QWEN_API_KEY", "")
        self.model = model or os.getenv("QWEN_MODEL", "qwen-turbo")
        self.local_base_url = local_base_url or os.getenv("QWEN_LOCAL_BASE_URL", "")
        self.local_model = local_model or os.getenv("QWEN_LOCAL_MODEL", "qwen2.5:7b")
        self.max_retries = max_retries
        self.cache_enabled = cache_enabled
        self._cache: dict[str, dict] = {}
        self._is_local = bool(self.local_base_url)

    # ── 单条分析 ──────────────────────────────────────────

    def analyze_sentiment(self, text: str) -> dict:
        """
        分析单条文本的情绪。

        返回:
            {"sentiment_score": float, "impact_duration": str, "confidence": float,
             "key_topics": list, "affected_factors": list}
        """
        if not text or not text.strip():
            return self._empty_result()

        cache_key = str(hash(text))
        if self.cache_enabled and cache_key in self._cache:
            return self._cache[cache_key]

        for attempt in range(self.max_retries):
            try:
                if self._is_local:
                    result = self._call_ollama(text)
                else:
                    result = self._call_dashscope(text)

                if result and "sentiment_score" in result:
                    if self.cache_enabled:
                        self._cache[cache_key] = result
                    return result

            except Exception as e:
                if attempt < self.max_retries - 1:
                    time.sleep(1 * (attempt + 1))
                else:
                    print(f"  [ERROR] Qwen 分析失败(重试{self.max_retries}次): {e}")

        return self._empty_result()

    # ── 批量分析 ──────────────────────────────────────────

    def analyze_batch(
        self,
        texts: list[str],
        batch_size: int = 10,
        progress_callback: callable = None,  # type: ignore
    ) -> list[dict]:
        """
        批量分析（逐条调用，节省并发成本）。

        参数:
            texts: 文本列表
            batch_size: 每批次数量
            progress_callback: 进度回调 fn(done, total)

        返回:
            [{"sentiment_score": ..., ...}, ...]
        """
        results = []
        total = len(texts)
        for i, text in enumerate(texts):
            result = self.analyze_sentiment(text)
            results.append(result)
            if progress_callback:
                progress_callback(i + 1, total)
            # 批次间短暂休息，避免触发限频
            if (i + 1) % batch_size == 0 and i < total - 1:
                time.sleep(0.5)
        return results

    # ── DashScope API ──────────────────────────────────────

    def _call_dashscope(self, text: str) -> dict:
        """调用 DashScope API。"""
        resp = requests.post(
            "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
            },
            json={
                "model": self.model,
                "messages": [
                    {"role": "system", "content": self.SYSTEM_PROMPT},
                    {"role": "user", "content": text[:4000]},  # 截断长文本
                ],
                "temperature": 0.1,
                "max_tokens": 500,
            },
            timeout=30,
        )
        resp.raise_for_status()
        body = resp.json()
        content = body["choices"][0]["message"]["content"]
        return self._parse_json(content)

    # ── Ollama 本地调用 ────────────────────────────────────

    def _call_ollama(self, text: str) -> dict:
        """调用本地 Ollama。"""
        resp = requests.post(
            f"{self.local_base_url.rstrip('/')}/chat/completions",
            json={
                "model": self.local_model,
                "messages": [
                    {"role": "system", "content": self.SYSTEM_PROMPT},
                    {"role": "user", "content": text[:4000]},
                ],
                "temperature": 0.1,
                "max_tokens": 500,
            },
            timeout=60,
        )
        resp.raise_for_status()
        body = resp.json()
        content = body["choices"][0]["message"]["content"]
        return self._parse_json(content)

    # ── JSON 解析 ──────────────────────────────────────────

    @staticmethod
    def _parse_json(content: str) -> dict:
        """从模型输出中提取 JSON。"""
        # 尝试直接解析
        try:
            return json.loads(content)
        except json.JSONDecodeError:
            pass
        # 尝试提取 ```json ... ``` 代码块
        if "```json" in content:
            start = content.find("```json") + 7
            end = content.find("```", start)
            if end > start:
                try:
                    return json.loads(content[start:end].strip())
                except json.JSONDecodeError:
                    pass
        # 尝试找 { ... }
        brace_start = content.find("{")
        brace_end = content.rfind("}")
        if brace_start >= 0 and brace_end > brace_start:
            try:
                return json.loads(content[brace_start:brace_end + 1])
            except json.JSONDecodeError:
                pass

        return QwenClient._empty_result()

    @staticmethod
    def _empty_result() -> dict:
        return {
            "sentiment_score": 0.0,
            "impact_duration": "short",
            "confidence": 0.0,
            "key_topics": [],
            "affected_factors": [],
        }
