from __future__ import annotations

import re
from datetime import date, datetime, timedelta
from typing import Any

from backend.bootstrap.config import normalize_date, validate_text
from backend.data.providers.ifind_client import IfindError
from backend.features.mentor.agent import MentorAgentError, stream_with_mentor


MENTOR_DATA_PROFILES = {
    "emotion": {
        "kobe92-perspective", "niepanchongsheng-perspective",
        "chaojiyangjia-perspective", "tuixuechaogu-perspective",
        "chenxiaoqun-perspective", "zhiyechaoshou-perspective",
    },
    "first_board": {
        "beijingchaojia-perspective", "chuangshiji-perspective",
        "xuxiang-perspective", "foshanwuyingjiao-perspective",
    },
    "leader": {
        "zhaolaoge-perspective", "fangxinxia-perspective",
        "xiaoe-perspective", "sunge-perspective", "liuyizhonglu-perspective",
    },
    "trend": {
        "zhangdetao-perspective", "zhangmengzhu-perspective",
        "zuoshouxinyi-perspective",
    },
    "low_absorption": {
        "qiaobangzhu-perspective", "asking-perspective",
        "longfeihu-perspective", "ruihexian-perspective",
    },
    "macro": {"shuipi-perspective"},
}

MENTOR_INDEX_UNIVERSE = (
    ("000001.SH", "上证指数"), ("399001.SZ", "深证成指"),
    ("399006.SZ", "创业板指"), ("000016.SH", "上证50"),
    ("000300.SH", "沪深300"), ("000905.SH", "中证500"),
    ("000852.SH", "中证1000"), ("932000.CSI", "中证2000"),
)

MENTOR_ETF_UNIVERSE = (
    ("510050.SH", "上证50ETF"), ("510300.SH", "沪深300ETF"),
    ("510500.SH", "中证500ETF"), ("512100.SH", "中证1000ETF"),
)


class MentorServiceMixin:
    def mentor_setup(self, trade_date: str) -> dict[str, Any]:
        normalized_date = normalize_date(trade_date)
        mentors = [
            skill.public()
            for skill in self.mentor_skills.list_skills(
                include_private=self.membership()["is_admin"]
            )
        ]
        if not mentors:
            raise ValueError("游资skills 目录中没有可用的 SKILL.md。")
        stored_preferences = self.database.list_mentor_preferences(self.current_user_id)
        preferences = {item["mentor_id"]: item for item in stored_preferences}
        for default_order, mentor in enumerate(mentors):
            preference = preferences.get(str(mentor.get("id") or ""), {})
            mentor["pinned"] = bool(preference.get("pinned"))
            mentor["sort_order"] = int(preference.get("sort_order", 10000 + default_order))
        mentors.sort(
            key=lambda item: (
                not bool(item.get("pinned")),
                int(item.get("sort_order") or 0),
            )
        )
        for sort_order, mentor in enumerate(mentors):
            mentor["sort_order"] = sort_order
        snapshot = self.database.get_snapshot(normalized_date)
        actual_date = str((snapshot or {}).get("meta", {}).get("trade_date") or normalized_date)
        return {
            "trade_date": actual_date,
            "mentors": mentors,
            "preferences_configured": bool(stored_preferences),
            "llm": {
                "configured": self.llm_configured,
                "model": self.llm_primary_model if self.llm_configured else "",
                "fallback_configured": self.llm_fallback_configured,
                "fallback_model": self.llm_fallback_model if self.llm_fallback_configured else "",
            },
        }

    def save_mentor_preferences(self, payload: dict[str, Any]) -> dict[str, Any]:
        available_ids = [
            skill.skill_id
            for skill in self.mentor_skills.list_skills(
                include_private=self.membership()["is_admin"]
            )
        ]
        available = set(available_ids)
        raw_order = payload.get("order")
        raw_pinned = payload.get("pinned")
        if not isinstance(raw_order, list) or not isinstance(raw_pinned, list):
            raise ValueError("问师排序格式不正确。")
        ordered_ids: list[str] = []
        for raw_id in raw_order:
            mentor_id = validate_text(raw_id, "问师角色", 100, required=True)
            if mentor_id not in available:
                raise ValueError("问师排序中包含不可用的思维模型。")
            if mentor_id not in ordered_ids:
                ordered_ids.append(mentor_id)
        ordered_ids.extend(mentor_id for mentor_id in available_ids if mentor_id not in ordered_ids)
        pinned_ids = {
            validate_text(raw_id, "问师角色", 100, required=True)
            for raw_id in raw_pinned
        }
        if not pinned_ids.issubset(available):
            raise ValueError("问师置顶中包含不可用的思维模型。")
        self.database.save_mentor_preferences(
            self.current_user_id, ordered_ids, pinned_ids
        )
        return {"saved": True}

    def mentor_stream(self, payload: dict[str, Any]):
        mentor_id = validate_text(payload.get("mentor_id"), "问师角色", 100, required=True)
        question = validate_text(payload.get("question"), "问题", 2000, required=True)
        trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat()))
        history = self._validate_mentor_history(payload.get("history") or [])
        skill = self.mentor_skills.get_skill(
            mentor_id, include_private=self.membership()["is_admin"]
        )
        context = self._build_mentor_context(trade_date, question, skill)

        def generate():
            answer_parts: list[str] = []
            follow_ups: list[str] = []
            events = self.llm_gateway.stream(
                "mentor",
                f"mentor-skill-v2:{skill.skill_id}",
                lambda profile: stream_with_mentor(
                    skill,
                    context,
                    question,
                    history,
                    profile.api_key,
                    profile.base_url,
                    profile.model,
                    follow_ups=follow_ups,
                ),
                (MentorAgentError,),
            )
            for event in events:
                if event.kind == "delta":
                    chunk = str(event.value or "")
                    answer_parts.append(chunk)
                    yield {"type": "delta", "content": chunk}
                elif event.kind == "complete":
                    self.database.save_mentor_exchange(
                        self.current_user_id,
                        mentor_id,
                        trade_date,
                        question,
                        "".join(answer_parts).strip(),
                        context["data_trade_date"],
                    )
                    yield {
                        "type": "meta",
                        "data_trade_date": context["data_trade_date"],
                        "follow_ups": follow_ups or self._mentor_follow_up_fallback(question),
                        "notice": "智能解读已自动切换可用服务。"
                        if event.role == "fallback"
                        else "",
                    }

        return generate()

    @staticmethod
    def _mentor_follow_up_fallback(question: str) -> list[str]:
        normalized = question.strip()
        if any(keyword in normalized for keyword in ("风险", "亏损", "回撤", "止损")):
            return [
                "这些风险最早会从哪些信号中暴露？",
                "哪些变化会让当前风险判断失效？",
                "如果风险继续扩大，仓位预案应如何调整？",
            ]
        if any(keyword in normalized for keyword in ("股票", "个股", "代码", "怎么看")):
            return [
                "这个判断最关键的确认信号是什么？",
                "哪些变化会让当前结论失效？",
                "明日盘中应该优先观察哪些数据？",
            ]
        return [
            "这个判断最关键的确认依据是什么？",
            "哪些变化会让当前结论失效？",
            "下一步应该优先观察什么？",
        ]

    def mentor_messages(self, mentor_id: str, trade_date: str) -> list[dict[str, Any]]:
        mentor_id = validate_text(mentor_id, "问师角色", 100, required=True)
        trade_date = normalize_date(trade_date)
        self.mentor_skills.get_skill(
            mentor_id, include_private=self.membership()["is_admin"]
        )
        return self.database.list_mentor_messages(
            self.current_user_id, mentor_id, trade_date
        )

    def clear_mentor_messages(self, mentor_id: str, trade_date: str) -> int:
        mentor_id = validate_text(mentor_id, "问师角色", 100, required=True)
        trade_date = normalize_date(trade_date)
        self.mentor_skills.get_skill(
            mentor_id, include_private=self.membership()["is_admin"]
        )
        return self.database.delete_mentor_messages(
            self.current_user_id, mentor_id, trade_date
        )

    @staticmethod
    def _validate_mentor_history(raw_history: Any) -> list[dict[str, str]]:
        if not isinstance(raw_history, list):
            raise ValueError("问师对话历史格式不正确。")
        history = []
        total_length = 0
        for item in raw_history[-12:]:
            if not isinstance(item, dict) or item.get("role") not in {"user", "assistant"}:
                raise ValueError("问师对话历史包含无效消息。")
            content = str(item.get("content") or "").strip()
            if not content or len(content) > 5000:
                raise ValueError("问师对话历史消息为空或过长。")
            total_length += len(content)
            if total_length > 24_000:
                raise ValueError("问师对话历史过长，请清空后重新提问。")
            history.append({"role": item["role"], "content": content})
        return history

    def _build_mentor_context(
        self, trade_date: str, question: str, skill: Any | None = None
    ) -> dict[str, Any]:
        dashboard = self.get_dashboard(trade_date)
        data_trade_date = normalize_date(
            str(dashboard.get("meta", {}).get("trade_date") or trade_date)
        )
        regime = self.screener.detect_regime(data_trade_date)
        limits = list(dashboard.get("limits") or [])
        broken = list(dashboard.get("broken") or [])
        down_limits = list(dashboard.get("down_limits") or [])
        yesterday_limits = list(dashboard.get("yesterday_limits") or [])
        all_stocks = limits + broken + down_limits + yesterday_limits
        matched_rows = []
        codes = re.findall(r"(?<!\d)\d{6}(?!\d)", question)[:3]
        for row in all_stocks:
            code = str(row.get("code") or "")
            name = str(row.get("name") or "")
            if code in codes or (len(name) >= 2 and name in question):
                if not any(item.get("code") == code for item in matched_rows):
                    matched_rows.append(row)
        for row in matched_rows:
            code = str(row.get("code") or "")
            if code and code not in codes:
                codes.append(code)
        stock_details = []
        for code in codes[:2]:
            try:
                detail = self.get_stock_detail(code, data_trade_date)
                stock_details.append(
                    {
                        "stock": detail.get("stock") or {},
                        "moneyflow": detail.get("moneyflow") or {},
                        "recent_prices": (detail.get("prices") or [])[-20:],
                    }
                )
            except Exception as exc:
                stock_details.append({"code": code, "error": str(exc)})

        skill_id = str(getattr(skill, "skill_id", "") or "")
        profile = next(
            (
                profile_name
                for profile_name, skill_ids in MENTOR_DATA_PROFILES.items()
                if skill_id in skill_ids
            ),
            "balanced",
        )
        dragon_tiger = None
        if any(keyword in question for keyword in ("龙虎榜", "席位", "机构", "游资")):
            try:
                dragon_payload = self.get_dragon_tiger(data_trade_date)
                rows = list(dragon_payload.get("rows") or [])
                matched_dragon = [row for row in rows if str(row.get("code") or "") in codes]
                leading_dragon = sorted(
                    rows,
                    key=lambda row: abs(float(row.get("net_buy_million") or 0)),
                    reverse=True,
                )[:12]
                dragon_tiger = {
                    "summary": dragon_payload.get("summary") or {},
                    "matched": matched_dragon,
                    "largest_net_flows": leading_dragon,
                }
            except Exception as exc:
                dragon_tiger = {"error": str(exc)}

        context: dict[str, Any] = {
            "data_trade_date": data_trade_date,
            "data_profile": profile,
            "overview": dashboard.get("overview") or {},
            "market_regime": regime,
            "recent_market_history": self.database.snapshot_summaries(data_trade_date, 10),
            "question_matched_stocks": matched_rows[:10],
            "stock_details": stock_details,
        }

        ordered_limits = sorted(
            limits,
            key=lambda row: (
                float(row.get("streak") or 0),
                float(row.get("amount_billion") or 0),
            ),
            reverse=True,
        )
        if profile in {"emotion", "balanced"}:
            context.update(
                {
                    "limit_ladder": dashboard.get("ladders") or [],
                    "limit_performance": dashboard.get("limit_performance") or [],
                    "hot_sectors": (dashboard.get("sectors") or [])[:15],
                    "sector_rotation": (dashboard.get("sector_rotation") or [])[:15],
                    "limit_up_stocks": ordered_limits[:30],
                    "broken_stocks": sorted(
                        broken,
                        key=lambda row: float(row.get("amount_billion") or 0),
                        reverse=True,
                    )[:20],
                    "limit_down_stocks": down_limits[:20],
                    "yesterday_limit_performance": sorted(
                        yesterday_limits,
                        key=lambda row: float(row.get("change") or 0),
                        reverse=True,
                    )[:20],
                }
            )
        elif profile == "first_board":
            context.update(
                {
                    "first_board_environment": {
                        "seal_rate": (dashboard.get("overview") or {}).get("seal_rate"),
                        "broken_count": len(broken),
                        "first_boards": [row for row in ordered_limits if int(row.get("streak") or 1) == 1][:35],
                        "broken_stocks": sorted(
                            broken,
                            key=lambda row: float(row.get("amount_billion") or 0),
                            reverse=True,
                        )[:30],
                    },
                    "hot_sectors": (dashboard.get("sectors") or [])[:12],
                }
            )
        elif profile == "leader":
            context.update(
                {
                    "limit_ladder": dashboard.get("ladders") or [],
                    "multi_board_leaders": [
                        row for row in ordered_limits if int(row.get("streak") or 0) >= 2
                    ][:25],
                    "hot_sectors": (dashboard.get("sectors") or [])[:12],
                    "sector_rotation": (dashboard.get("sector_rotation") or [])[:12],
                }
            )
            try:
                popularity = self.popularity(data_trade_date)
                context["popularity_core"] = {
                    "consensus": [
                        row for row in (popularity.get("combined") or [])
                        if row.get("dual_source")
                    ][:10],
                    "ths": (popularity.get("ths") or [])[:10],
                    "eastmoney": (popularity.get("dc") or [])[:10],
                }
            except Exception:
                context["popularity_core"] = {"unavailable": True}
        elif profile == "trend":
            context.update(
                {
                    "index_momentum": self._mentor_market_matrix(
                        data_trade_date, MENTOR_INDEX_UNIVERSE
                    ),
                    "sector_rotation": (dashboard.get("sector_rotation") or [])[:20],
                    "hot_sectors": (dashboard.get("sectors") or [])[:20],
                    "market_breadth": {
                        key: (dashboard.get("overview") or {}).get(key)
                        for key in ("up_count", "down_count", "flat_count", "amount_billion")
                    },
                }
            )
        elif profile == "low_absorption":
            context.update(
                {
                    "yesterday_limit_performance": sorted(
                        yesterday_limits,
                        key=lambda row: float(row.get("change") or 0),
                        reverse=True,
                    )[:35],
                    "broken_stocks": broken[:20],
                    "hot_sectors": (dashboard.get("sectors") or [])[:12],
                }
            )
        elif profile == "macro":
            context.update(
                {
                    "broad_indexes": self._mentor_market_matrix(
                        data_trade_date, MENTOR_INDEX_UNIVERSE
                    ),
                    "core_etfs": self._mentor_market_matrix(
                        data_trade_date, MENTOR_ETF_UNIVERSE
                    ),
                    "market_style": {
                        "amount_billion": (dashboard.get("overview") or {}).get("amount_billion"),
                        "breadth": {
                            "up": (dashboard.get("overview") or {}).get("up_count"),
                            "down": (dashboard.get("overview") or {}).get("down_count"),
                        },
                        "top_sectors": (dashboard.get("sectors") or [])[:15],
                    },
                    "unavailable_data": [
                        "政策原文与隔夜资讯尚未接入",
                        "汇率、利率和商品宏观序列当前不可用",
                    ],
                }
            )
        if dragon_tiger is not None:
            context["dragon_tiger"] = dragon_tiger
        return context

    def _mentor_market_matrix(
        self, trade_date: str, universe: tuple[tuple[str, str], ...]
    ) -> list[dict[str, Any]]:
        ifind = getattr(self, "ifind", None)
        if not ifind or not ifind.configured:
            return []
        end = datetime.strptime(trade_date, "%Y%m%d")
        start = (end - timedelta(days=45)).strftime("%Y%m%d")
        names = {code: name for code, name in universe}
        try:
            rows = ifind.history(
                list(names), ["close", "volume", "amount"], start, trade_date, cache_ttl=600
            )
        except IfindError:
            return []
        grouped: dict[str, list[dict[str, Any]]] = {}
        for row in rows:
            code = str(row.get("thscode") or "").upper()
            if code in names:
                grouped.setdefault(code, []).append(row)
        result = []
        for code, name in universe:
            series = sorted(grouped.get(code, []), key=lambda row: str(row.get("time") or ""))
            closes = []
            for row in series:
                try:
                    close = float(row.get("close") or 0)
                except (TypeError, ValueError):
                    continue
                if close > 0:
                    closes.append(close)
            if not closes:
                continue
            def period_return(days: int) -> float | None:
                if len(closes) <= days or closes[-days - 1] <= 0:
                    return None
                return round((closes[-1] / closes[-days - 1] - 1) * 100, 2)
            previous = closes[-2] if len(closes) > 1 else 0
            result.append(
                {
                    "code": code,
                    "name": name,
                    "close": round(closes[-1], 3),
                    "change": round((closes[-1] / previous - 1) * 100, 2) if previous else None,
                    "return_5d": period_return(5),
                    "return_10d": period_return(10),
                    "return_20d": period_return(20),
                    "latest_amount": series[-1].get("amount") if series else None,
                }
            )
        return result
