"""
情绪因子计算引擎。

全链路：数据获取 → Qwen 分析 → 因子计算 → 缓存管理。
"""

import json
import os
import time
from datetime import datetime
from pathlib import Path

import pandas as pd

from data.data_manager import DataManager
from factors.sentiment.news_source import NewsSource, align_news_to_trading_days
from factors.sentiment.qwen_client import QwenClient
from factors.sentiment.sentiment_factor import (
    NewsSentimentFactor,
    SentimentMomentumFactor,
    SentimentConfidenceFactor,
)


def _get_or_fetch_price(dm, ts_code, fallback_code="000001.SZ"):
    """
    获取交易日历（价格 DataFrame）。

    1. 优先从 DB 缓存读取目标股票
    2. 无缓存则 sync_daily 补齐
    3. 补齐失败则 fallback 到备用股票（仅用作交易日历，不影响新闻分析）
    """
    import pandas as pd
    from database.dao import get_latest_trade_date

    # 检查 DB 缓存
    if get_latest_trade_date(ts_code):
        daily = dm.get_daily(ts_code)
        if daily is not None and not daily.empty:
            return daily.set_index("trade_date").sort_index()

    # 无缓存 → 尝试补齐
    print("  [SentimentEngine] {} 无 DB 缓存，尝试 sync_daily...".format(ts_code))
    try:
        n = dm.sync_daily(ts_code)
        if n > 0:
            daily = dm.get_daily(ts_code)
            if daily is not None and not daily.empty:
                return daily.set_index("trade_date").sort_index()
    except Exception as e:
        print("  [SentimentEngine] sync_daily 失败: {}".format(e))

    # Fallback
    print("  [SentimentEngine] 使用 {} 交易日历作为 fallback".format(fallback_code))
    try:
        fallback = dm.get_daily(fallback_code)
        if fallback is not None and not fallback.empty:
            return fallback.set_index("trade_date").sort_index()
    except Exception:
        pass

    return None


class SentimentEngine:
    """
    情绪因子计算引擎。

    参数:
        data_manager: DataManager 实例
        qwen_client: QwenClient 实例（可选，默认从环境变量创建）
        news_source: NewsSource 实例（可选）
        cache_dir: 缓存目录
    """

    def __init__(
        self,
        data_manager: DataManager,
        qwen_client: QwenClient | None = None,
        news_source: NewsSource | None = None,
        cache_dir: str | None = None,
    ):
        self._dm = data_manager
        self._qwen = qwen_client or QwenClient()
        self._news_source = news_source or NewsSource()
        self._cache_dir = Path(cache_dir or os.path.join(
            os.path.dirname(__file__), "..", "..", ".cache", "sentiment"
        ))
        self._cache_dir.mkdir(parents=True, exist_ok=True)

    # ── 核心方法 ──────────────────────────────────────────

    def compute(
        self,
        ts_code: str,
        start: str | None = None,
        end: str | None = None,
        max_news: int = 20,
    ) -> pd.DataFrame:
        """
        计算单只股票的情绪因子（含缓存）。

        返回:
            DataFrame (trade_date, news_sent_5, news_conf_5, sent_delta_5)
        """
        from database.dao import get_latest_trade_date

        # 1. 获取交易日历：优先目标股票 DB 缓存，无则尝试补齐，失败则 fallback
        price = _get_or_fetch_price(self._dm, ts_code)
        if price is None:
            return pd.DataFrame()
        daily_idx = pd.to_datetime(price.index, format="%Y%m%d", errors="coerce")

        # 2. 检查缓存
        cache_key = "sent_{}_{}_{}".format(ts_code, start or "all", end or "now")
        cached = self._load_cache(cache_key)
        if cached is not None:
            return cached

        # 3. 获取新闻
        max_n = max_news or int(os.getenv("SENTIMENT_MAX_NEWS_PER_STOCK", "30"))
        news_df = self._news_source.fetch(ts_code, start=start, end=end, max_news=max_n)

        # 4. 对齐到交易日
        if not news_df.empty:
            news_df = align_news_to_trading_days(news_df, daily_idx)

        # 5. Qwen 情绪分析（有 API key 时才执行）
        sentiment_df = self._analyze_news(news_df)

        # 6. 计算因子
        factor_dfs = {}
        if not sentiment_df.empty:
            for factor_cls, kwargs in [
                (NewsSentimentFactor, {"window": 5, "sentiment_df": sentiment_df}),
                (SentimentConfidenceFactor, {"window": 5, "sentiment_df": sentiment_df}),
                (SentimentMomentumFactor, {"period": 5, "sentiment_df": sentiment_df}),
            ]:
                f = factor_cls(**kwargs)
                series = f.calculate(price)
                factor_dfs[f.name] = series

        if factor_dfs:
            result = pd.DataFrame(factor_dfs)
        else:
            result = pd.DataFrame(index=price.index)

        # 7. 缓存
        self._save_cache(cache_key, result)

        return result

    def compute_batch(
        self,
        ts_codes: list[str],
        date: str | None = None,
        max_news: int = 20,
    ) -> dict[str, pd.DataFrame]:
        """
        批量计算多只股票的情绪因子。

        返回:
            {ts_code: factor_df}
        """
        results = {}
        total = len(ts_codes)
        for i, ts_code in enumerate(ts_codes):
            try:
                results[ts_code] = self.compute(
                    ts_code, start=None if date is None else date, end=date,
                    max_news=max_news,
                )
            except Exception as e:
                print(f"[WARN] {ts_code} 情绪因子失败: {e}")
            if (i + 1) % 10 == 0:
                print(f"[SentimentEngine] {i + 1}/{total}")
        return results

    # ── 分析范围解析 ──────────────────────────────────────

    def get_scope_stocks(self) -> list[str]:
        """
        根据 .env 配置解析分析范围。

        优先级: custom > index > sector > all
        """
        scope_type = os.getenv("SENTIMENT_SCOPE_TYPE", "index")
        all_stocks = self._dm.get_stock_list()

        if scope_type == "custom":
            codes = os.getenv("SENTIMENT_SCOPE_CUSTOM", "")
            return [c.strip() for c in codes.split(",") if c.strip()]

        if scope_type == "sector":
            return self._get_sector_stocks(all_stocks)

        if scope_type == "index":
            return self._get_index_stocks()

        # all
        return list(all_stocks.index)

    def _get_index_stocks(self) -> list[str]:
        """根据指数成分股获取股票列表。"""
        indexes_str = os.getenv("SENTIMENT_SCOPE_INDEXES", "000300")
        indexes = [i.strip() for i in indexes_str.split(",") if i.strip()]
        stocks = set()
        import akshare as ak
        for idx_code in indexes:
            try:
                df = ak.index_stock_cons(symbol=idx_code)
                if df is not None and not df.empty:
                    # 列名可能是 '品种代码' 或 'stock_code'
                    col = next((c for c in df.columns if "代码" in c or "code" in c.lower()), df.columns[0])
                    for code in df[col]:
                        stocks.add(f"{code}.SZ" if code.startswith("0") or code.startswith("3") else f"{code}.SH")
            except Exception as e:
                print(f"  [WARN] 指数 {idx_code} 成分股获取失败: {e}")
        return list(stocks)

    def _get_sector_stocks(self, all_stocks: pd.DataFrame) -> list[str]:
        """根据板块名称筛选股票。"""
        sectors_str = os.getenv("SENTIMENT_SCOPE_SECTORS", "")
        sectors = [s.strip() for s in sectors_str.split(",") if s.strip()]
        if not sectors:
            return list(all_stocks.index)
        # 利用 AkShare 行业分类筛选
        import akshare as ak
        all_industry = ak.stock_board_industry_name_em()
        matched = all_industry[all_industry["板块名称"].isin(sectors)]
        stocks = set()
        for _, row in matched.iterrows():
            try:
                df = ak.stock_board_industry_cons_em(symbol=row["板块名称"])
                if df is not None and not df.empty:
                    code_col = next((c for c in df.columns if "代码" in c), df.columns[0])
                    for code in df[code_col]:
                        stocks.add(code)
            except Exception:
                continue
        return list(stocks)

    # ── 新闻情绪分析 ──────────────────────────────────────

    def _analyze_news(self, news_df: pd.DataFrame) -> pd.DataFrame:
        """
        对新闻列表执行情绪分析。

        返回:
            DataFrame (date, sentiment_score, confidence, impact_duration, key_topics, title, content)
        """
        if news_df.empty:
            return pd.DataFrame()

        # 检查是否有 Qwen API 配置
        if not self._qwen.api_key and not self._qwen.local_base_url:
            return pd.DataFrame()

        results = []
        for _, row in news_df.iterrows():
            title = row.get("title", "")
            content = row.get("content", "")
            # 优先用标题+内容，文本过短时只用标题
            text = f"{title}\n{content}" if len(str(content)) > 20 else title
            if len(text.strip()) < 10:
                continue

            analysis = self._qwen.analyze_sentiment(text)
            results.append({
                "date": row["date"],
                "title": title,
                "sentiment_score": analysis.get("sentiment_score", 0.0),
                "confidence": analysis.get("confidence", 0.0),
                "impact_duration": analysis.get("impact_duration", "short"),
                "key_topics": json.dumps(analysis.get("key_topics", [])),
            })

        if not results:
            return pd.DataFrame()

        return pd.DataFrame(results)

    # ── 缓存管理 ──────────────────────────────────────────

    def _load_cache(self, key: str) -> pd.DataFrame | None:
        """加载缓存。缓存有效期 6 小时。"""
        cache_file = self._cache_dir / f"{key.replace('/', '_')}.parquet"
        if not cache_file.exists():
            return None
        mtime = cache_file.stat().st_mtime
        if time.time() - mtime > 6 * 3600:
            return None
        try:
            return pd.read_parquet(cache_file)
        except Exception:
            return None

    def _save_cache(self, key: str, df: pd.DataFrame) -> None:
        cache_file = self._cache_dir / f"{key.replace('/', '_')}.parquet"
        try:
            df.to_parquet(cache_file)
        except Exception:
            pass

    def clear_cache(self) -> int:
        """清空缓存，返回删除文件数。"""
        count = 0
        for f in self._cache_dir.glob("*.parquet"):
            f.unlink()
            count += 1
        return count
