from __future__ import annotations

from statistics import median
from typing import Any

from backend.data.numbers import non_nan_number as _number
from backend.features.market.insights_context import _display_date


class MarketAuctionScoringMixin:
    @staticmethod
    def _expectation_label(actual_strength: float, expected_change: float) -> str:
        difference = actual_strength - expected_change
        if difference >= 1.5:
            return "超预期"
        if difference <= -1.5:
            return "低于预期"
        return "符合预期"

    @staticmethod
    def _auction_confirmation(row: dict[str, Any]) -> float:
        volume_ratio = _number(row.get("volume_ratio"))
        turnover_rate = _number(row.get("turnover_rate"))
        amount_million = _number(row.get("amount_million"))
        return (
            (0.6 if volume_ratio >= 2 else 0.3 if volume_ratio >= 1.2 else -0.5 if volume_ratio < 0.6 else 0)
            + (0.25 if turnover_rate >= 0.15 else -0.25 if turnover_rate < 0.03 else 0)
            + (0.3 if amount_million >= 20 else 0.15 if amount_million >= 5 else -0.3 if amount_million < 1 else 0)
        )

    @staticmethod
    def _attention_score(
        row: dict[str, Any],
        expected_change: float,
        core_tags: list[str],
        sources: list[str],
        prior_streak: int,
        strong_sector: bool,
    ) -> float:
        if core_tags:
            identity_score = 35.0
        elif prior_streak >= 2:
            identity_score = 27.0
        elif any(source in {"昨日涨停", "昨日炸板"} for source in sources):
            identity_score = 21.0
        else:
            identity_score = 14.0
        deviation_score = min(30.0, abs(_number(row.get("change")) - expected_change) * 5)
        volume_score = min(10.0, max(0.0, _number(row.get("volume_ratio"))) / 2 * 10)
        amount_score = min(6.0, max(0.0, _number(row.get("amount_million"))) / 10 * 6)
        turnover_score = min(4.0, max(0.0, _number(row.get("turnover_rate"))) / 0.2 * 4)
        theme_score = 15.0 if strong_sector else 7.0 if row.get("concepts") else 0.0
        return round(min(100.0, identity_score + deviation_score + volume_score + amount_score + turnover_score + theme_score), 1)

    def _auction_candidates(
        self,
        rows: list[dict[str, Any]],
        baseline_date: str,
    ) -> tuple[list[dict[str, Any]], dict[str, Any], list[dict[str, Any]]]:
        """Build a narrow, explainable universe from prior limits, breaks and top-20 hot lists."""
        snapshot = self.database.get_snapshot(baseline_date) or {}
        prior_limits = list(snapshot.get("limits") or [])
        prior_broken = list(snapshot.get("broken") or [])
        prior_sectors = list(snapshot.get("sectors") or [])
        strong_sector_names = {
            str(item.get("name") or "") for item in prior_sectors[:5] if item.get("name")
        }
        ths_rows, dc_rows, errors = self._hot_rows(baseline_date)
        candidates: dict[str, dict[str, Any]] = {}
        core_tags: dict[str, set[str]] = {}

        def ensure_candidate(item: dict[str, Any]) -> dict[str, Any] | None:
            code = str(item.get("code") or str(item.get("ts_code") or "").split(".")[0])
            if not code:
                return None
            return candidates.setdefault(
                code,
                {
                    "sources": [],
                    "streak": 0,
                    "sector": str(item.get("sector") or "其他"),
                    "name": str(item.get("name") or item.get("ts_name") or "--"),
                    "concepts": [],
                    "ths_rank": None,
                    "dc_rank": None,
                },
            )

        for item in prior_limits:
            candidate = ensure_candidate(item)
            if candidate is None:
                continue
            candidate["sources"].append("昨日涨停")
            candidate["streak"] = max(1, int(_number(item.get("streak"), 1)))

        for item in prior_broken:
            candidate = ensure_candidate(item)
            if candidate is not None and "昨日炸板" not in candidate["sources"]:
                candidate["sources"].append("昨日炸板")

        limit_streaks = [max(1, int(_number(item.get("streak"), 1))) for item in prior_limits]
        highest_streak = max(limit_streaks, default=0)
        for item in prior_limits:
            code = str(item.get("code") or "")
            streak = max(1, int(_number(item.get("streak"), 1)))
            if streak >= 3:
                core_tags.setdefault(code, set()).add("三板以上")
            if highest_streak and streak == highest_streak:
                core_tags.setdefault(code, set()).add("市场最高板")

        for sector in prior_sectors[:5]:
            name = str(sector.get("name") or "")
            members = [item for item in prior_limits if str(item.get("sector") or "其他") == name]
            if not members:
                continue
            leader = max(
                members,
                key=lambda item: (
                    int(_number(item.get("streak"), 1)),
                    _number(item.get("amount_billion")),
                    -_number(item.get("open_times")),
                ),
            )
            core_tags.setdefault(str(leader.get("code") or ""), set()).add("题材核心")

        leadership = sorted(
            prior_limits,
            key=lambda item: (
                int(_number(item.get("streak"), 1)),
                str(item.get("sector") or "") in strong_sector_names,
                _number(item.get("amount_billion")),
            ),
            reverse=True,
        )
        if leadership:
            core_tags.setdefault(str(leadership[0].get("code") or ""), set()).add("市场领涨")

        hot_records: dict[str, dict[str, Any]] = {}

        for source, hot_rows, data_type in (
            ("同花顺热榜", ths_rows, "热股"),
            ("东方财富热榜", dc_rows, "A股市场"),
        ):
            for item in hot_rows:
                if str(item.get("data_type") or "") != data_type:
                    continue
                ts_code = str(item.get("ts_code") or "")
                code = ts_code.split(".")[0]
                rank = max(1, int(_number(item.get("rank"), 9999)))
                if not code or rank > 20:
                    continue
                hot = hot_records.setdefault(
                    code,
                    {
                        "name": str(item.get("ts_name") or "--"),
                        "concepts": [],
                        "ths_rank": None,
                        "dc_rank": None,
                    },
                )
                hot["ths_rank" if source == "同花顺热榜" else "dc_rank"] = rank
                if source == "同花顺热榜":
                    hot["concepts"] = self._parse_concepts(item.get("concept"))

        ranked_hot = sorted(
            hot_records.items(),
            key=lambda pair: (
                ((21 - (pair[1].get("ths_rank") or 21)) / 20)
                + ((21 - (pair[1].get("dc_rank") or 21)) / 20)
                + (0.35 if pair[1].get("ths_rank") and pair[1].get("dc_rank") else 0)
            ),
            reverse=True,
        )
        for code, _ in ranked_hot[:5]:
            core_tags.setdefault(code, set()).add("人气前5")

        for code, hot in hot_records.items():
            ranks = [rank for rank in (hot.get("ths_rank"), hot.get("dc_rank")) if isinstance(rank, int)]
            dual = len(ranks) == 2
            if not ranks or (min(ranks) > 10 and not dual and code not in candidates and code not in core_tags):
                continue
            candidate = candidates.setdefault(
                code,
                {
                    "sources": [],
                    "streak": 0,
                    "sector": "其他",
                    "name": hot["name"],
                    "concepts": [],
                    "ths_rank": None,
                    "dc_rank": None,
                },
            )
            candidate["ths_rank"] = hot.get("ths_rank")
            candidate["dc_rank"] = hot.get("dc_rank")
            candidate["concepts"] = hot.get("concepts") or []
            if hot.get("ths_rank") and "同花顺热榜" not in candidate["sources"]:
                candidate["sources"].append("同花顺热榜")
            if hot.get("dc_rank") and "东方财富热榜" not in candidate["sources"]:
                candidate["sources"].append("东方财富热榜")

        normalized = []
        for row in rows:
            candidate = candidates.get(str(row.get("code") or ""))
            if not candidate:
                continue
            streak = int(candidate["streak"])
            expected_change = {1: 1.5, 2: 3.0, 3: 4.0}.get(streak, 5.0 if streak else 0.5)
            ranks = [
                rank for rank in (candidate.get("ths_rank"), candidate.get("dc_rank"))
                if isinstance(rank, int)
            ]
            if len(ranks) == 2:
                expected_change += 0.8
            elif ranks:
                best_rank = min(ranks)
                expected_change += 0.7 if best_rank <= 10 else 0.4 if best_rank <= 30 else 0.2
            expected_change = min(expected_change, 6.5)

            volume_ratio = _number(row.get("volume_ratio"))
            turnover_rate = _number(row.get("turnover_rate"))
            amount_million = _number(row.get("amount_million"))
            confirmation = self._auction_confirmation(row)
            actual_strength = _number(row.get("change")) + confirmation
            label = self._expectation_label(actual_strength, expected_change)
            is_broken = "昨日炸板" in candidate["sources"] and "昨日涨停" not in candidate["sources"]
            identity = f"昨日{streak}板" if streak > 1 else "昨日首板" if streak == 1 else "昨日炸板" if is_broken else "人气榜标的"
            popularity = "，双榜共识" if len(ranks) == 2 else "，热榜靠前" if ranks and min(ranks) <= 10 else ""
            difference = _number(row.get("change")) - expected_change
            direction = "高于" if difference > 0 else "低于" if difference < 0 else "贴合"
            reason = (
                f"{identity}{popularity}；竞价涨幅{direction}预期中枢"
                f"{abs(difference):.1f}个百分点，量比{volume_ratio:.2f}"
            )
            tags = sorted(core_tags.get(str(row.get("code") or ""), set()))
            scored_row = {
                **row,
                "concepts": candidate["concepts"],
            }
            attention_score = self._attention_score(
                scored_row,
                expected_change,
                tags,
                candidate["sources"],
                streak,
                str(candidate.get("sector") or row.get("sector") or "") in strong_sector_names,
            )
            normalized.append(
                {
                    **scored_row,
                    "sector": candidate["sector"] if candidate["sector"] != "其他" else row.get("sector", "其他"),
                    "candidate_sources": candidate["sources"],
                    "source_label": " · ".join(candidate["sources"]),
                    "prior_streak": streak,
                    "concepts": candidate["concepts"],
                    "expected_change": round(expected_change, 2),
                    "actual_strength": round(actual_strength, 2),
                    "expectation": label,
                    "attention_score": attention_score,
                    "core_tags": tags,
                    "is_market_core": bool(tags),
                    "expectation_reason": reason,
                }
            )
        normalized.sort(key=lambda item: (_number(item.get("attention_score")), _number(item.get("amount_million"))), reverse=True)
        matched_top = {
            str(item.get("code") or "")
            for item in sorted(
                (item for item in normalized if item.get("expectation") == "符合预期"),
                key=lambda item: _number(item.get("attention_score")),
                reverse=True,
            )[:20]
        }
        focus_candidates = [
            item for item in normalized
            if item.get("is_market_core")
            or (_number(item.get("attention_score")) >= 55 and item.get("expectation") != "符合预期")
            or str(item.get("code") or "") in matched_top
        ]
        mandatory = [item for item in focus_candidates if item.get("is_market_core")]
        mandatory_codes = {str(item.get("code") or "") for item in mandatory}
        optional = [item for item in focus_candidates if str(item.get("code") or "") not in mandatory_codes]
        focus_rows = sorted(mandatory, key=lambda item: _number(item.get("attention_score")), reverse=True)
        focus_rows.extend(optional[:max(0, 30 - len(focus_rows))])
        focus_rows.sort(key=lambda item: _number(item.get("attention_score")), reverse=True)
        return normalized, {
            "baseline_date": _display_date(baseline_date),
            "prior_limit_count": len(prior_limits),
            "prior_broken_count": len(prior_broken),
            "hot_candidate_count": sum(
                any(source in {"同花顺热榜", "东方财富热榜"} for source in item["sources"])
                for item in candidates.values()
            ),
            "core_count": sum(bool(item.get("is_market_core")) for item in normalized),
            "notice": "；".join(errors),
        }, focus_rows

    @staticmethod
    def _auction_theme_evidence(
        prior_snapshot: dict[str, Any],
        candidate_rows: list[dict[str, Any]],
    ) -> dict[str, list[dict[str, Any]]]:
        prior_sectors = list(prior_snapshot.get("sectors") or [])
        carry = []
        for sector in prior_sectors[:10]:
            name = str(sector.get("name") or "其他")
            matched = [row for row in candidate_rows if str(row.get("sector") or "其他") == name]
            changes = [_number(row.get("change")) for row in matched]
            middle = median(changes) if changes else -10.0
            positive_rate = sum(value > 0.2 for value in changes) / len(changes) * 100 if changes else 0.0
            if middle >= 2 and positive_rate >= 60:
                status = "强承接"
            elif middle >= 0 and positive_rate >= 50:
                status = "有承接"
            elif middle > -2:
                status = "分歧"
            else:
                status = "承接弱"
            carry.append(
                {
                    "name": name,
                    "status": status,
                    "prior_limit_count": int(_number(sector.get("count"))),
                    "leader": str(sector.get("leader") or "--"),
                    "matched_count": len(matched),
                    "median_change": round(middle, 2) if matched else None,
                    "positive_rate": round(positive_rate, 1),
                    "amount_million": round(sum(_number(row.get("amount_million")) for row in matched), 2),
                }
            )

        concept_groups: dict[str, list[dict[str, Any]]] = {}
        prior_names = {str(item.get("name") or "") for item in prior_sectors}
        for row in candidate_rows:
            for concept in row.get("concepts") or []:
                if concept and concept not in prior_names:
                    concept_groups.setdefault(str(concept), []).append(row)
        new_themes = []
        for name, members in concept_groups.items():
            unique = {str(item.get("code") or ""): item for item in members}
            values = list(unique.values())
            changes = [_number(item.get("change")) for item in values]
            if len(values) < 2 or median(changes) < 2 or sum(value > 0.2 for value in changes) / len(values) < 0.67:
                continue
            new_themes.append(
                {
                    "name": name,
                    "stock_count": len(values),
                    "median_change": round(median(changes), 2),
                    "amount_million": round(sum(_number(item.get("amount_million")) for item in values), 2),
                    "leaders": [str(item.get("name") or "--") for item in sorted(values, key=lambda value: _number(value.get("change")), reverse=True)[:3]],
                }
            )
        new_themes.sort(key=lambda item: (item["stock_count"], item["median_change"], item["amount_million"]), reverse=True)
        return {"carry": carry, "new_themes": new_themes[:8]}
