"""
SelectionAgent — 多因子股票打分。

综合技术因子 + 基本面因子 + ML 预测值 → 股票综合得分排序。
"""

import numpy as np
import pandas as pd

from agents.base import BaseAgent


class SelectionAgent(BaseAgent):
    """股票打分 Agent。"""

    name = "Selection"
    description = "多因子股票打分与推荐"

    def execute(
        self,
        date: str | None = None,
        ts_codes: list[str] | None = None,
        top_n: int = 15,
        weighting: str = "equal",
    ) -> dict:
        """
        对股票池打分排序。

        参数:
            date: 目标日期，None=最新
            ts_codes: 股票池，None=指数成分股
            top_n: 返回 Top N
            weighting: 'equal' | 'ic_weighted' | 'ml'

        返回:
            {"date": ..., "top_picks": [...], "score_df": DataFrame}
        """
        from factors.registry import get_factor, list_factors

        # 确定股票池
        if ts_codes is None:
            if self.sent:
                ts_codes = self.sent.get_scope_stocks()
            else:
                stocks = self.dm.get_stock_list()
                ts_codes = list(stocks.index[:100])

        if not ts_codes:
            return {"date": date or self._today(), "top_picks": [], "score_df": pd.DataFrame()}

        # 选择核心因子（覆盖多个维度，减少计算量）
        core_factors = [
            "momentum_20", "momentum_60",
            "rsi_14", "volatility_20",
            "vol_ratio_5", "ma_dev_20",
            "turnover_5", "amplitude_5",
        ]
        factor_objects = [get_factor(n) for n in core_factors]

        self.log("打分 {} 只股票 (权重={})".format(len(ts_codes), weighting))

        # 1. 筛选有 DB 缓存的股票（Orchestrator 已在 Step1 同步了全量范围）
        from database.dao import get_latest_trade_date
        available = []
        for ts_code in sorted(ts_codes):
            if get_latest_trade_date(ts_code):
                available.append(ts_code)
        self.log("缓存命中: {}/{} ({:.1f}%)".format(
            len(available), len(ts_codes),
            len(available) / len(ts_codes) * 100 if ts_codes else 0))

        # 2. 打分（限制上限防止单次太慢）
        score_limit = min(len(available), 300)
        scores = {}
        valid_count = 0
        for i, ts_code in enumerate(available[:score_limit]):
            try:
                score = self._score_stock(ts_code, factor_objects, date, weighting)
                if score is not None:
                    scores[ts_code] = score
                    valid_count += 1
            except Exception:
                continue
            if (i + 1) % 50 == 0:
                self.log("  进度: {}/{}".format(i + 1, score_limit))

        self.log("有效评分: {}/{}".format(len(scores), score_limit))

        if not scores:
            return {"date": date or self._today(), "top_picks": [], "score_df": pd.DataFrame()}

        # 排序
        sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True)
        top = sorted_scores[:top_n]

        # 取股票名称（ts_code 是 index）
        stock_list = self.dm.get_stock_list() if self.dm else pd.DataFrame()
        name_map = {}
        if not stock_list.empty and "name" in stock_list.columns:
            name_map = dict(zip(stock_list.index, stock_list["name"]))

        top_picks = []
        for ts_code, score in top:
            # 尝试多种格式匹配名称
            code_no_suffix = ts_code.replace(".SZ", "").replace(".SH", "").replace(".BJ", "")
            name = name_map.get(ts_code, name_map.get(code_no_suffix, ts_code))
            top_picks.append({
                "ts_code": ts_code,
                "name": name,
                "score": round(score, 4),
            })

        score_df = pd.DataFrame(
            {"ts_code": list(scores.keys()), "score": list(scores.values())}
        ).sort_values("score", ascending=False).reset_index(drop=True)

        return {
            "date": date or self._today(),
            "top_picks": top_picks,
            "score_df": score_df,
            "universe_size": len(ts_codes),
            "valid_scores": valid_count,
        }

    def _filter_cached_stocks(self, ts_codes: list[str], limit: int = 100) -> list[str]:
        """筛选有 DB 日线缓存的股票，避免逐个调用 AkShare。"""
        from database.dao import get_latest_trade_date
        cached = []
        for ts_code in ts_codes[:limit]:
            latest = get_latest_trade_date(ts_code)
            if latest:
                cached.append(ts_code)
        return cached

    def _score_stock(
        self,
        ts_code: str,
        factors: list,
        date: str | None,
        weighting: str,
    ) -> float | None:
        """对单只股票打分。"""
        daily = self.dm.get_daily(ts_code)
        if daily is None or daily.empty:
            return None
        daily = daily.set_index("trade_date").sort_index()

        factor_df = self.fe.compute(ts_code, factors)
        if factor_df is None or factor_df.empty:
            return None

        if date and date in factor_df.index:
            row = factor_df.loc[date]
        else:
            row = factor_df.iloc[-1]  # 最新一天

        if row.isna().all():
            return None

        if weighting == "ml" and self.ml_models:
            return self._score_ml(ts_code, factor_df, date)

        # 等权打分：标准化因子值后求和
        row_clean = row.dropna()
        if len(row_clean) < 3:
            return None

        # z-score 标准化
        z = (row_clean - factor_df[row_clean.index].mean()) / factor_df[row_clean.index].std().replace(0, 1)
        return float(z.mean())

    def _score_ml(self, ts_code: str, factor_df: pd.DataFrame, date: str | None) -> float | None:
        """ML 模型打分。"""
        from models.features import FeatureEngine
        fe = FeatureEngine(lookahead=5)

        daily = self.dm.get_daily(ts_code)
        if daily is None or daily.empty:
            return None
        daily = daily.set_index("trade_date")

        try:
            X, _ = fe.build(factor_df, daily, fit=False)
            if X.empty:
                return None
            if date and date in X.index:
                X = X.loc[[date]]
            else:
                X = X.iloc[[-1]]

            model = self.ml_models.get("lightgbm") or list(self.ml_models.values())[0]
            pred = model.predict(X)
            return float(pred.iloc[0]) if len(pred) > 0 else None
        except Exception:
            return None
