"""
情绪因子。

将 Qwen 输出的情绪分数转换为量化因子值。
"""

import numpy as np
import pandas as pd

from factors.base import BaseFactor


class NewsSentimentFactor(BaseFactor):
    """
    新闻情绪因子。

    将多条新闻的情绪分数按时间加权聚合到每个交易日。

    参数:
        window: 滚动窗口（交易日）
        decay: 指数衰减系数（越大衰减越快），0 表示等权
        sentiment_df: 情绪分析结果 DataFrame
                      (date, sentiment_score, confidence, title, content)
    """

    category = "sentiment"

    def __init__(self, window: int = 5, decay: float = 0.3, sentiment_df: pd.DataFrame | None = None):
        self.window = window
        self.decay = decay
        self.sentiment_df = sentiment_df
        self.name = f"news_sent_{window}"

    def calculate(self, df: pd.DataFrame) -> pd.Series:
        if self.sentiment_df is None or self.sentiment_df.empty:
            return pd.Series(float("nan"), index=df.index, name=self.name)

        return _aggregate_sentiment(
            df, self.sentiment_df, self.window, self.decay
        )

    def get_required_columns(self) -> list[str]:
        return []


class SentimentMomentumFactor(BaseFactor):
    """
    情绪动量因子。

    当前情绪 - N 日前情绪，衡量情绪变化方向。
    """

    category = "sentiment"

    def __init__(self, period: int = 5, sentiment_df: pd.DataFrame | None = None):
        self.period = period
        self.sentiment_df = sentiment_df
        self.name = f"sent_delta_{period}"

    def calculate(self, df: pd.DataFrame) -> pd.Series:
        base = NewsSentimentFactor(window=1, sentiment_df=self.sentiment_df).calculate(df)
        return base.diff(self.period)

    def get_required_columns(self) -> list[str]:
        return []


class SentimentConfidenceFactor(BaseFactor):
    """
    情绪置信度因子。

    新闻情绪分析的置信度越高，因子绝对值越大（方向同 sentiment_score）。
    sentiment_score × confidence → 高置信利好=正值大，高置信利空=负值大。
    """

    category = "sentiment"

    def __init__(self, window: int = 5, sentiment_df: pd.DataFrame | None = None):
        self.window = window
        self.sentiment_df = sentiment_df
        self.name = f"news_conf_{window}"

    def calculate(self, df: pd.DataFrame) -> pd.Series:
        if self.sentiment_df is None or self.sentiment_df.empty:
            return pd.Series(float("nan"), index=df.index, name=self.name)

        sdf = self.sentiment_df.copy()
        # 设置加权分数
        if "confidence" in sdf.columns and "sentiment_score" in sdf.columns:
            sdf["weighted_score"] = sdf["sentiment_score"] * sdf["confidence"]
        else:
            return pd.Series(float("nan"), index=df.index, name=self.name)

        return _aggregate_sentiment(df, sdf, self.window, decay=0.3, score_col="weighted_score")

    def get_required_columns(self) -> list[str]:
        return []


# ── 情绪聚合工具函数 ──────────────────────────────────────

def _aggregate_sentiment(
    daily_df: pd.DataFrame,
    sentiment_df: pd.DataFrame,
    window: int,
    decay: float,
    score_col: str = "sentiment_score",
) -> pd.Series:
    """
    将情绪分数按时间加权聚合到交易日。

    逻辑：
    1. 对每个交易日 t，找到 [t - window + 1, t] 范围内的所有新闻
    2. 按 time_decay = exp(-decay * days_from_t) 加权
    3. 按 confidence（如有）加权
    4. 返回加权平均情绪分数

    参数:
        daily_df: 日线 DataFrame（提供 index 和日期对齐）
        sentiment_df: 情绪 DataFrame（date 列 + score_col）
        window: 窗口大小
        decay: 衰减系数
        score_col: 情绪分数列名
    """
    if sentiment_df.empty:
        return pd.Series(float("nan"), index=daily_df.index, name=score_col)

    # 统一日期格式
    sdf = sentiment_df.copy()
    sdf["date"] = pd.to_datetime(sdf["date"], format="%Y%m%d", errors="coerce")
    sdf = sdf.dropna(subset=["date"])
    sdf = sdf.sort_values("date")

    daily_idx = pd.to_datetime(daily_df.index, format="%Y%m%d", errors="coerce")
    if daily_idx.isna().all():
        daily_idx = pd.to_datetime(daily_df.index)

    result = pd.Series(float("nan"), index=daily_df.index)

    # 对 news 日期建立搜索索引
    news_dates = sdf["date"].values

    for i, dt in enumerate(daily_idx):
        if pd.isna(dt):
            continue
        # 窗口起始
        window_start = dt - pd.Timedelta(days=window * 2)  # 宽窗覆盖非交易日

        mask = (news_dates >= window_start) & (news_dates <= dt)
        candidates = sdf[mask]

        if candidates.empty:
            continue

        # 时间衰减权重
        days_diff = (dt - candidates["date"]).dt.days
        time_weights = np.exp(-decay * days_diff)

        # 置信度权重（如有）
        conf_weights = candidates.get("confidence", pd.Series(1.0, index=candidates.index)).fillna(0.5)
        scores = candidates[score_col].fillna(0.0)

        total_weight = (time_weights * conf_weights).sum()
        if total_weight > 0:
            result.iloc[i] = (scores * time_weights * conf_weights).sum() / total_weight

    result.name = score_col
    return result.astype("float64")
