"""
新闻公告数据源。

支持三种数据来源：
1. AkShare stock_news_em（东方财富个股新闻）
2. MariaDB xwlb_daily_ext（新闻联播分割数据）
3. MCP trendradar-news（外部新闻聚合服务）

统一返回格式：DataFrame (date, title, content, source, url)
"""

import json
import os
import time
from datetime import datetime, timedelta

import akshare as ak
import pandas as pd
import requests

# 确保 .env 已加载
import config.settings  # noqa: F401


class NewsSource:
    """
    新闻数据源聚合器。

    参数:
        use_akshare: 启用 AkShare 新闻接口
        use_xwlb: 启用新闻联播数据库
        use_mcp: 启用 MCP 新闻服务
        mcp_url: MCP 服务地址
        request_delay: 请求间隔（秒），避免被限频
    """

    def __init__(
        self,
        use_akshare: bool = True,
        use_xwlb: bool = True,
        use_mcp: bool = True,
        mcp_url: str | None = None,
        request_delay: float = 1.0,
    ):
        self.use_akshare = use_akshare
        self.use_xwlb = use_xwlb
        self.use_mcp = use_mcp
        self.mcp_url = mcp_url or os.getenv("NEWS_MCP_URL", "http://192.168.1.160:3333/mcp")
        self.request_delay = request_delay
        self._mcp_session_id: str | None = None

    # ── 统一入口 ──────────────────────────────────────────

    def fetch(
        self,
        ts_code: str,
        start: str | None = None,
        end: str | None = None,
        max_news: int | None = None,
    ) -> pd.DataFrame:
        """
        获取个股相关新闻（聚合多源）。

        参数:
            ts_code: 如 '000001.SZ'
            start: 起始日期 'YYYYMMDD'，默认 30 天前
            end: 结束日期 'YYYYMMDD'，默认今天
            max_news: 最多返回条数，默认从 .env SENTIMENT_MAX_NEWS_PER_STOCK 读取，fallback 30

        返回:
            DataFrame (date, title, content, source, url)
        """
        if max_news is None:
            max_news = int(os.getenv("SENTIMENT_MAX_NEWS_PER_STOCK", "30"))

        if end is None:
            end = time.strftime("%Y%m%d")
        if start is None:
            start = (datetime.strptime(end, "%Y%m%d") - timedelta(days=30)).strftime("%Y%m%d")

        frames = []

        if self.use_akshare:
            try:
                df = self._fetch_akshare(ts_code, start, end)
                if not df.empty:
                    frames.append(df)
                    time.sleep(self.request_delay)
            except Exception as e:
                print("  [WARN] AkShare 新闻获取失败 ({}): {}".format(ts_code, e))

        if self.use_xwlb:
            try:
                # xwlb: news_date 是播出日，+1day = 交易日
                # 所以查询时 start/end 各减 1 天
                xwlb_start = (datetime.strptime(start, "%Y%m%d") - timedelta(days=1)).strftime("%Y%m%d")
                xwlb_end = (datetime.strptime(end, "%Y%m%d") - timedelta(days=1)).strftime("%Y%m%d")
                df = self._fetch_xwlb(xwlb_start, xwlb_end)
                if not df.empty:
                    frames.append(df)
            except Exception as e:
                print("  [WARN] xwlb 新闻获取失败: {}".format(e))

        if self.use_mcp:
            try:
                df = self._fetch_mcp(ts_code, start, end)
                if not df.empty:
                    frames.append(df)
            except Exception as e:
                print("  [WARN] MCP 新闻获取失败: {}".format(e))

        if not frames:
            return pd.DataFrame(columns=["date", "title", "content", "source", "url"])

        result = pd.concat(frames, ignore_index=True)
        result = result.drop_duplicates(subset=["title", "date"])
        result = result.sort_values("date", ascending=False)

        if len(result) > max_news:
            result = result.head(max_news)

        return result.reset_index(drop=True)

    # ── AkShare 数据源 ────────────────────────────────────

    def _fetch_akshare(self, ts_code: str, start: str = "", end: str = "") -> pd.DataFrame:
        """
        东方财富个股新闻。

        stock_news_em 返回最新约 10 条（无日期筛选），
        如需更多应使用 InfoMoney 或自建爬虫。
        """
        symbol = ts_code.replace(".SZ", "").replace(".SH", "").replace(".BJ", "")
        try:
            df = ak.stock_news_em(symbol=symbol.zfill(6))
        except Exception:
            return pd.DataFrame()

        if df is None or df.empty:
            return pd.DataFrame()

        df = df.rename(columns={
            "新闻标题": "title",
            "新闻内容": "content",
            "发布时间": "date",
            "文章来源": "source",
            "新闻链接": "url",
        })
        df["date"] = pd.to_datetime(df["date"]).dt.strftime("%Y%m%d")
        df["source"] = "akshare_" + df["source"].fillna("unknown")

        # 日期筛选
        if start:
            df = df[df["date"] >= start]
        if end:
            df = df[df["date"] <= end]

        return df[["date", "title", "content", "source", "url"]]

    # ── 新闻联播数据源 ────────────────────────────────────

    def _fetch_xwlb(self, start: str, end: str) -> pd.DataFrame:
        """
        从 MariaDB xwlb_daily_ext 表获取新闻联播分割数据。

        news_date 是播出日期，+1day 后成为对市场产生影响的交易日。
        调用方应传入 (target_start-1, target_end-1)。

        连接失败时 database/connection.py 自动重建 SSH 隧道。
        """
        from database.connection import get_engine

        start_fmt = "{}-{}-{}".format(start[:4], start[4:6], start[6:8])
        end_fmt = "{}-{}-{}".format(end[:4], end[4:6], end[6:8])

        day_span = (datetime.strptime(end, "%Y%m%d") - datetime.strptime(start, "%Y%m%d")).days
        row_limit = max(100, (day_span + 1) * 40)

        sql = """
            SELECT news_date, news_title, news_content
            FROM xwlb_daily_ext
            WHERE news_date BETWEEN %(start)s AND %(end)s
            ORDER BY news_date DESC
            LIMIT {}
        """.format(row_limit)

        try:
            df = pd.read_sql(
                sql, get_engine(),
                params={"start": start_fmt, "end": end_fmt},
            )
            if df.empty:
                return pd.DataFrame()

            df = df.rename(columns={
                "news_date": "date",
                "news_title": "title",
                "news_content": "content",
            })
            df["date"] = (
                pd.to_datetime(df["date"]) + pd.Timedelta(days=1)
            ).dt.strftime("%Y%m%d")
            df["source"] = "xwlb"
            df["url"] = ""
            return df[["date", "title", "content", "source", "url"]]

        except Exception:
            return pd.DataFrame()

    # ── MCP 数据源 ────────────────────────────────────────

    def _fetch_mcp(self, ts_code: str, start: str, end: str) -> pd.DataFrame:
        """通过 MCP streamable HTTP 调用 trendradar-news 服务。"""
        session_id = self._mcp_initialize()
        if not session_id:
            return pd.DataFrame()

        symbol = ts_code.replace(".SZ", "").replace(".SH", "").replace(".BJ", "")
        try:
            result = self._mcp_call_tool(
                session_id,
                "search_news",
                {"keyword": symbol, "start_date": start, "end_date": end, "limit": 30},
            )
            if not result:
                return pd.DataFrame()

            records = self._parse_mcp_result(result)
            if not records:
                return pd.DataFrame()

            df = pd.DataFrame(records)
            df["source"] = "mcp_trendradar"
            return df[["date", "title", "content", "source", "url"]]
        except Exception:
            return pd.DataFrame()

    def _mcp_initialize(self) -> str | None:
        """MCP streamable HTTP 初始化，获取 session ID。"""
        if self._mcp_session_id:
            return self._mcp_session_id
        try:
            resp = requests.post(
                self.mcp_url,
                json={
                    "jsonrpc": "2.0",
                    "method": "initialize",
                    "params": {
                        "protocolVersion": "2024-11-05",
                        "capabilities": {},
                        "clientInfo": {"name": "cc-cursor", "version": "1.0"},
                    },
                    "id": 1,
                },
                headers={
                    "Content-Type": "application/json",
                    "Accept": "application/json, text/event-stream",
                },
                timeout=10,
            )
            # headers 是大小写不敏感的，尝试多种格式
            session_id = (
                resp.headers.get("mcp-session-id")
                or resp.headers.get("Mcp-Session-Id")
                or resp.headers.get("MCP-Session-Id")
            )
            if session_id:
                self._mcp_session_id = session_id
            else:
                print("  [WARN] MCP initialize 未返回 session-id")
            return session_id
        except Exception as e:
            print("  [WARN] MCP 连接失败: {}".format(e))
            return None

    def _mcp_call_tool(
        self, session_id: str, tool_name: str, arguments: dict
    ) -> dict | None:
        """调用 MCP 工具。"""
        try:
            resp = requests.post(
                self.mcp_url,
                json={
                    "jsonrpc": "2.0",
                    "method": "tools/call",
                    "params": {"name": tool_name, "arguments": arguments},
                    "id": 2,
                },
                headers={
                    "Content-Type": "application/json",
                    "Accept": "application/json, text/event-stream",
                    "mcp-session-id": session_id,
                },
                timeout=15,
            )
            # 解析 SSE 响应
            for line in resp.text.split("\n"):
                if line.startswith("data:"):
                    data = json.loads(line[5:].strip())
                    return data.get("result", {})
            return None
        except Exception:
            return None

    @staticmethod
    def _parse_mcp_result(result: dict) -> list[dict]:
        """解析 MCP 返回内容为统一格式。"""
        records = []
        content = result.get("content", [])
        for item in content:
            text = item.get("text", "") if isinstance(item, dict) else str(item)
            if text:
                try:
                    parsed = json.loads(text)
                    if isinstance(parsed, list):
                        for p in parsed:
                            records.append({
                                "date": str(p.get("date", p.get("publish_date", ""))),
                                "title": str(p.get("title", "")),
                                "content": str(p.get("content", p.get("summary", ""))),
                                "url": str(p.get("url", "")),
                            })
                    elif isinstance(parsed, dict):
                        records.append({
                            "date": str(parsed.get("date", parsed.get("publish_date", ""))),
                            "title": str(parsed.get("title", "")),
                            "content": str(parsed.get("content", parsed.get("summary", ""))),
                            "url": str(parsed.get("url", "")),
                        })
                except (json.JSONDecodeError, TypeError):
                    records.append({
                        "date": time.strftime("%Y%m%d"),
                        "title": text[:100],
                        "content": text,
                        "url": "",
                    })
        return records


# ── 日期对齐工具 ──────────────────────────────────────────

def align_news_to_trading_days(
    news_df: pd.DataFrame,
    trading_calendar: pd.DatetimeIndex,
) -> pd.DataFrame:
    """
    将新闻日期对齐到最近交易日。

    非交易日的新闻归入最近的下一个交易日。
    例：周六新闻 → 下周一交易日的因子值中体现。

    参数:
        news_df: 新闻 DataFrame，含 'date' 列 (YYYYMMDD)
        trading_calendar: 交易日 DatetimeIndex

    返回:
        news_df，'date' 列已替换为对齐后的交易日
    """
    calendar = trading_calendar.sort_values()

    news_dates = pd.to_datetime(news_df["date"], format="%Y%m%d")
    aligned = []

    for nd in news_dates:
        future_dates = calendar[calendar >= nd]
        if len(future_dates) > 0:
            aligned.append(future_dates[0])
        else:
            aligned.append(calendar[-1])

    news_df = news_df.copy()
    news_df["date"] = pd.DatetimeIndex(aligned).strftime("%Y%m%d")
    return news_df
