from __future__ import annotations

import copy
from typing import Any

from backend.bootstrap.config import validate_text
from backend.data.providers.tushare_client import _sector_coverage_issue
from backend.features.heaven.engine import _market_line_scores, _score_to_line


class HeavenManualMixin:
    @staticmethod
    def _heaven_manual_schema(market_mode: str) -> dict[str, dict[str, Any]]:
        intraday = market_mode == "intraday"
        fields = {
            "stock_amount_percentile": {"line": 1, "label": "成交额全市场分位", "unit": "%", "min": 0, "max": 100},
            "stock_turnover_rate": {"line": 1, "label": "个股换手率", "unit": "%", "min": 0, "max": 100},
            "stock_turnover_relative": {"line": 1, "label": "相对市场换手", "unit": "倍", "min": 0, "max": 20},
            "stock_volume_activity_ratio": {"line": 1, "label": "同进度量能", "unit": "倍", "min": 0, "max": 20},
            "stock_seal_amount_million": {"line": 1, "label": "封单金额", "unit": "万元", "min": 0, "max": 100000000},
            "stock_open_times": {"line": 1, "label": "开板次数", "unit": "次", "min": 0, "max": 100, "integer": True},
            "stock_change": {"line": 2, "label": "个股涨跌幅", "unit": "%", "min": -100, "max": 100},
            "stock_streak": {"line": 2, "label": "连板高度", "unit": "板", "min": 0, "max": 100, "integer": True},
            "stock_status": {"line": 2, "label": "个股状态", "type": "select", "options": ["普通", "涨停", "炸板", "跌停"]},
            "sector_name": {"line": [3, 4], "label": "申万二级行业", "type": "text", "max_length": 50},
            "sector_up_count": {"line": 3, "label": "行业上涨家数", "unit": "家", "min": 0, "max": 10000, "integer": True},
            "sector_down_count": {"line": 3, "label": "行业下跌家数", "unit": "家", "min": 0, "max": 10000, "integer": True},
            "sector_coverage": {"line": 3, "label": "成分行情覆盖率", "unit": "%", "min": 0, "max": 100},
            "sector_relative_turnover": {"line": 3, "label": "行业相对市场换手", "unit": "倍", "min": 0, "max": 20},
            "sector_member_equal_change": {"line": 3, "label": "成分等权涨跌幅", "unit": "%", "min": -100, "max": 100},
            "sector_change": {"line": 4, "label": "申万官方涨跌幅", "unit": "%", "min": -100, "max": 100},
            "sector_leading_pct": {"line": [3, 4], "label": "行业领涨股涨跌幅", "unit": "%", "min": -100, "max": 100},
            "market_sentiment_score": {"line": 5, "label": "市场情绪温度", "unit": "分", "min": 0, "max": 100},
            "market_seal_rate": {"line": 5, "label": "封板率", "unit": "%", "min": 0, "max": 100},
            "market_amount_billion": {"line": 5, "label": "两市成交额", "unit": "亿元", "min": 0, "max": 10000000},
            "market_recent_average_amount_billion": {"line": 5, "label": "近期平均成交额", "unit": "亿元", "min": 0, "max": 10000000},
            "market_up_count": {"line": 5, "label": "上涨家数", "unit": "家", "min": 0, "max": 10000, "integer": True},
            "market_down_count": {"line": 5, "label": "下跌家数", "unit": "家", "min": 0, "max": 10000, "integer": True},
            "market_limit_up_count": {"line": 5, "label": "涨停家数", "unit": "家", "min": 0, "max": 10000, "integer": True},
            "market_limit_down_count": {"line": 5, "label": "跌停家数", "unit": "家", "min": 0, "max": 10000, "integer": True},
            "index_sh_change": {"line": 6, "label": "上证指数涨跌幅", "unit": "%", "min": -20, "max": 20},
            "index_sz_change": {"line": 6, "label": "深证成指涨跌幅", "unit": "%", "min": -20, "max": 20},
            "index_cy_change": {"line": 6, "label": "创业板指涨跌幅", "unit": "%", "min": -20, "max": 20},
            "note": {"line": [], "label": "补录说明", "type": "text", "max_length": 200},
        }
        if intraday:
            for key in ("stock_seal_amount_million", "stock_open_times"):
                fields.pop(key)
        else:
            for key in ("stock_turnover_relative", "stock_volume_activity_ratio", "sector_relative_turnover"):
                fields.pop(key)
        return fields

    @classmethod
    def _validate_heaven_manual_data(
        cls, raw: Any, market_mode: str
    ) -> dict[str, Any]:
        if raw in (None, ""):
            return {}
        if not isinstance(raw, dict):
            raise ValueError("六爻补录数据格式不正确。")
        schema = cls._heaven_manual_schema(market_mode)
        unknown = set(raw) - set(schema)
        if unknown:
            raise ValueError(f"六爻补录包含未知字段：{next(iter(sorted(unknown)))}")
        values: dict[str, Any] = {}
        for key, value in raw.items():
            if value is None or (isinstance(value, str) and not value.strip()):
                continue
            spec = schema[key]
            if spec.get("type") == "text":
                values[key] = validate_text(value, spec["label"], int(spec["max_length"]))
                continue
            if spec.get("type") == "select":
                text = str(value).strip()
                if text not in spec["options"]:
                    raise ValueError(f"{spec['label']}不在允许范围内。")
                values[key] = text
                continue
            try:
                number = float(value)
            except (TypeError, ValueError) as exc:
                raise ValueError(f"{spec['label']}必须是数字。") from exc
            if number < float(spec["min"]) or number > float(spec["max"]):
                raise ValueError(
                    f"{spec['label']}应在 {spec['min']} 至 {spec['max']} 之间。"
                )
            values[key] = int(number) if spec.get("integer") else number
        return values

    @staticmethod
    def _apply_heaven_manual_data(
        dashboard: dict[str, Any],
        index_context: dict[str, Any],
        sector: dict[str, Any] | None,
        stock: dict[str, Any] | None,
        manual_data: dict[str, Any],
        market_mode: str,
        trade_date: str,
        stock_code: str,
    ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]:
        dashboard = copy.deepcopy(dashboard)
        index_context = copy.deepcopy(index_context or {})
        sector = copy.deepcopy(sector or {})
        stock = copy.deepcopy(stock or {})
        overview = dashboard.setdefault("overview", {})

        stock_map = {
            "stock_amount_percentile": "amount_percentile",
            "stock_turnover_rate": "turnover_rate",
            "stock_turnover_relative": "turnover_relative",
            "stock_volume_activity_ratio": "volume_activity_ratio",
            "stock_seal_amount_million": "seal_amount_million",
            "stock_open_times": "open_times",
            "stock_change": "change",
            "stock_streak": "streak",
            "stock_status": "status",
        }
        sector_map = {
            "sector_name": "name",
            "sector_up_count": "up_count",
            "sector_down_count": "down_count",
            "sector_coverage": "coverage",
            "sector_relative_turnover": "relative_turnover",
            "sector_member_equal_change": "member_equal_change",
            "sector_change": "change",
            "sector_leading_pct": "leading_pct",
        }
        overview_map = {
            "market_sentiment_score": "sentiment_score",
            "market_seal_rate": "seal_rate",
            "market_amount_billion": "amount_billion",
            "market_recent_average_amount_billion": "recent_average_amount_billion",
            "market_up_count": "up_count",
            "market_down_count": "down_count",
            "market_limit_up_count": "limit_up_count",
            "market_limit_down_count": "limit_down_count",
        }
        for manual_key, target in stock_map.items():
            if manual_key in manual_data:
                stock[target] = manual_data[manual_key]
        for manual_key, target in sector_map.items():
            if manual_key in manual_data:
                sector[target] = manual_data[manual_key]
        for manual_key, target in overview_map.items():
            if manual_key in manual_data:
                overview[target] = manual_data[manual_key]

        if any(key.startswith("stock_") for key in manual_data):
            stock.setdefault("code", stock_code)
            stock.setdefault("name", stock_code or "--")
            stock["_quantitative_mode"] = "intraday" if market_mode == "intraday" else "historical"
            if market_mode == "intraday" and "stock_volume_activity_ratio" in manual_data:
                stock["activity_source"] = "user_supplied"
        if any(key.startswith("sector_") for key in manual_data):
            sector["_quantitative_mode"] = "intraday" if market_mode == "intraday" else "historical"
            sector.setdefault("taxonomy", "sw_l2")

        index_keys = (
            ("index_sh_change", "000001.SH", "上证指数"),
            ("index_sz_change", "399001.SZ", "深证成指"),
            ("index_cy_change", "399006.SZ", "创业板指"),
        )
        rows = {str(row.get("ts_code") or row.get("code") or ""): dict(row) for row in index_context.get("indices") or []}
        for manual_key, code, name in index_keys:
            if manual_key not in manual_data:
                continue
            row = rows.get(code, {"ts_code": code, "name": name})
            row.update({"pct_chg": manual_data[manual_key], "trade_date": trade_date})
            rows[code] = row
        ordered_rows = [rows.get(code) for _, code, _ in index_keys]
        if all(ordered_rows):
            index_context["indices"] = ordered_rows
            changes = [float(row.get("pct_chg") or 0) for row in ordered_rows]
            aggregate = dict(index_context.get("aggregate") or {})
            aggregate["average_pct_chg"] = sum(changes) / 3
            index_context["aggregate"] = aggregate
        return dashboard, index_context, sector, stock

    @classmethod
    def _heaven_line_checks(
        cls,
        trade_date: str,
        dashboard: dict[str, Any],
        recent_history: list[dict[str, Any]],
        index_context: dict[str, Any],
        sector: dict[str, Any],
        stock: dict[str, Any],
        market_mode: str,
        manual_data: dict[str, Any],
    ) -> list[dict[str, Any]]:
        intraday = market_mode == "intraday"
        closed = market_mode == "closed"
        schema = cls._heaven_manual_schema(market_mode)
        required = {
            1: (["stock_amount_percentile", "stock_turnover_relative", "stock_volume_activity_ratio"] if intraday else ["stock_amount_percentile", "stock_turnover_rate", "stock_seal_amount_million", "stock_open_times"]),
            2: ["stock_change", "stock_streak", "stock_status"],
            3: (["sector_name", "sector_up_count", "sector_down_count", "sector_coverage", "sector_relative_turnover"] if intraday else ["sector_name", "sector_up_count", "sector_down_count", "sector_coverage", "sector_member_equal_change", "sector_leading_pct"]),
            4: ["sector_name", "sector_change", "sector_leading_pct"],
            5: ["market_sentiment_score", "market_seal_rate", "market_amount_billion", "market_recent_average_amount_billion", "market_up_count", "market_down_count", "market_limit_up_count", "market_limit_down_count"],
            6: ["index_sh_change", "index_sz_change", "index_cy_change"],
        }
        names = {
            1: ("初爻", "个股内核", "成交活跃、换手与量能"),
            2: ("二爻", "个股外显", "涨跌、连板与状态"),
            3: ("三爻", "行业内核", "行业宽度与成交活跃"),
            4: ("四爻", "行业外显", "行业涨跌与领涨表现"),
            5: ("五爻", "市场内核", "情绪、封板、成交与市场宽度"),
            6: ("上爻", "指数外显", "三大指数当日涨跌"),
        }

        index_date = str(index_context.get("trade_date") or "").replace("-", "")
        index_rows = list(index_context.get("indices") or [])
        index_dates = {str(row.get("trade_date") or "").replace("-", "") for row in index_rows}
        index_issues = []
        if len(index_rows) < 3:
            index_issues.append(f"三大指数仅取得 {len(index_rows)}/3 条行情")
        elif index_date != trade_date or index_dates != {trade_date}:
            actual_dates = "、".join(sorted(value for value in index_dates if value)) or "未知"
            index_issues.append(f"指数实际日期为 {actual_dates}，目标交易日为 {trade_date}")
        elif not index_context.get("precise"):
            index_issues.append("三大指数行情未通过完整性校验")
        elif intraday and not index_context.get("realtime"):
            index_issues.append("盘中缺少可核验的实时指数行情")
        elif not intraday and (index_context.get("realtime") or str(index_context.get("source") or "") != "tushare"):
            index_issues.append("收盘或历史行情不是官方指数日线")

        sector_date = str(sector.get("trade_date") or "").replace("-", "")
        sector_coverage = float(sector.get("coverage") or 0)
        sector_explained_count = int(
            sector.get("explained_count")
            if sector.get("explained_count") is not None
            else sector.get("quote_count") or 0
        )
        sector_explained_coverage = float(
            sector.get("explained_coverage")
            if sector.get("explained_coverage") is not None
            else sector_coverage
        )
        sector_coverage_issue = _sector_coverage_issue(
            int(sector.get("member_count") or 0),
            int(sector.get("quote_count") or 0),
            sector_explained_coverage,
            sector_explained_count,
        )
        sector_common = []
        if not sector:
            sector_common.append("未取得申万二级行业归属")
        elif sector.get("taxonomy") != "sw_l2":
            sector_common.append("行业分类不是申万二级")
        elif sector_date != trade_date:
            sector_common.append("行业行情日期与目标交易日不一致")
        elif intraday and not sector.get("realtime"):
            sector_common.append("盘中行业行情不是申万实时行情")
        elif market_mode == "historical" and sector.get("realtime"):
            sector_common.append("历史行业行情不能使用实时快照")
        elif closed and sector.get("realtime") and not sector.get("finalized"):
            sector_common.append("收盘行业实时行情尚未形成15:00最终快照")
        sector_inner = list(sector_common)
        sector_outer = list(sector_common)
        if not sector.get("inner_precise", sector.get("precise")):
            sector_inner.append(str(sector.get("inner_error") or sector.get("error") or "行业内核数据未通过校验"))
        if not sector.get("outer_precise", sector.get("precise")):
            sector_outer.append(str(sector.get("outer_error") or sector.get("error") or "行业外显数据未通过校验"))
        if sector and sector_coverage_issue and sector_coverage_issue not in sector_inner:
            sector_inner.append(sector_coverage_issue)
        if sector.get("realtime") and not sector.get("relative_turnover"):
            sector_inner.append("缺少行业相对全市场换手活跃度")

        stock_date = str(stock.get("trade_date") or "").replace("-", "")
        stock_common = []
        if not stock.get("code"):
            stock_common.append("尚未载入有效个股")
        elif stock_date != trade_date:
            stock_common.append(f"个股实际日期为 {stock_date or '未知'}，目标交易日为 {trade_date}")
        elif not stock.get("precise"):
            stock_common.append("个股行情未通过完整性校验")
        elif intraday and not stock.get("realtime"):
            stock_common.append("盘中个股行情不是实时行情")
        elif not intraday and (stock.get("realtime") or str(stock.get("data_source") or "") != "tushare"):
            stock_common.append("收盘或历史个股行情不是官方日线")
        stock_inner = list(stock_common)
        if intraday and stock.get("turnover_source") in {None, "", "unavailable"}:
            stock_inner.append("缺少可核验的实时换手率")
        if intraday and stock.get("activity_source") in {None, "", "unavailable"}:
            stock_inner.append("缺少同时间进度量能基准")

        overview = dashboard.get("overview") or {}
        market_key_map = {
            "market_sentiment_score": "sentiment_score", "market_seal_rate": "seal_rate",
            "market_amount_billion": "amount_billion", "market_recent_average_amount_billion": "recent_average_amount_billion",
            "market_up_count": "up_count", "market_down_count": "down_count",
            "market_limit_up_count": "limit_up_count", "market_limit_down_count": "limit_down_count",
        }
        market_issues = []
        for manual_key, source_key in market_key_map.items():
            if source_key == "recent_average_amount_billion":
                history_values = [item.get("amount_billion") for item in recent_history[:-1] if item.get("amount_billion") is not None]
                if source_key not in overview and not history_values:
                    market_issues.append(f"缺少{schema[manual_key]['label']}")
            elif source_key not in overview or overview.get(source_key) is None:
                market_issues.append(f"缺少{schema[manual_key]['label']}")

        automatic_issues = {
            1: stock_inner, 2: stock_common, 3: sector_inner,
            4: sector_outer, 5: market_issues, 6: index_issues,
        }
        limits = list(dashboard.get("limits") or [])
        scores = _market_line_scores(dashboard, recent_history, index_context, sector, stock, limits)

        value_map: dict[str, Any] = {
            "stock_amount_percentile": stock.get("amount_percentile"),
            "stock_turnover_rate": stock.get("turnover_rate"),
            "stock_turnover_relative": stock.get("turnover_relative"),
            "stock_volume_activity_ratio": stock.get("volume_activity_ratio"),
            "stock_seal_amount_million": stock.get("seal_amount_million"),
            "stock_open_times": stock.get("open_times"),
            "stock_change": stock.get("change"), "stock_streak": stock.get("streak"),
            "stock_status": stock.get("status"), "sector_name": sector.get("name"),
            "sector_up_count": sector.get("up_count"), "sector_down_count": sector.get("down_count"),
            "sector_coverage": sector.get("coverage"), "sector_relative_turnover": sector.get("relative_turnover"),
            "sector_member_equal_change": sector.get("member_equal_change"),
            "sector_change": sector.get("change"), "sector_leading_pct": sector.get("leading_pct"),
            "market_sentiment_score": overview.get("sentiment_score"), "market_seal_rate": overview.get("seal_rate"),
            "market_amount_billion": overview.get("amount_billion"),
            "market_recent_average_amount_billion": overview.get("recent_average_amount_billion"),
            "market_up_count": overview.get("up_count"), "market_down_count": overview.get("down_count"),
            "market_limit_up_count": overview.get("limit_up_count"), "market_limit_down_count": overview.get("limit_down_count"),
        }
        history_values = [float(item.get("amount_billion")) for item in recent_history[:-1] if item.get("amount_billion") is not None]
        if value_map["market_recent_average_amount_billion"] is None and history_values:
            value_map["market_recent_average_amount_billion"] = sum(history_values) / len(history_values)
        if value_map["stock_amount_percentile"] is None and not intraday:
            amount = float(stock.get("amount_billion") or 0)
            amounts = [float(item.get("amount_billion") or 0) for item in limits if item.get("amount_billion") is not None]
            value_map["stock_amount_percentile"] = (
                sum(item <= amount for item in amounts) / len(amounts) * 100 if amounts else None
            )
        row_by_code = {str(row.get("ts_code") or row.get("code") or ""): row for row in index_context.get("indices") or []}
        value_map.update({
            "index_sh_change": (row_by_code.get("000001.SH") or {}).get("pct_chg"),
            "index_sz_change": (row_by_code.get("399001.SZ") or {}).get("pct_chg"),
            "index_cy_change": (row_by_code.get("399006.SZ") or {}).get("pct_chg"),
        })

        def missing_value(key: str) -> bool:
            value = value_map.get(key)
            return value is None or (isinstance(value, str) and not value.strip())

        invalid_fields = {
            line_number: {key for key in keys if missing_value(key)}
            for line_number, keys in required.items()
        }
        if stock_common:
            invalid_fields[1].update(required[1])
            invalid_fields[2].update(required[2])
        else:
            if intraday and stock.get("turnover_source") in {None, "", "unavailable"}:
                invalid_fields[1].add("stock_turnover_relative")
            if intraday and stock.get("activity_source") in {None, "", "unavailable"}:
                invalid_fields[1].add("stock_volume_activity_ratio")

        if sector_common:
            invalid_fields[3].update(required[3])
            invalid_fields[4].update(required[4])
        else:
            if not sector.get("inner_precise", sector.get("precise")) or sector_coverage_issue:
                invalid_fields[3].update(key for key in required[3] if key != "sector_name")
            if sector.get("realtime") and not sector.get("relative_turnover"):
                invalid_fields[3].add("sector_relative_turnover")
            # The official SW index supplies only the sector's external change. A valid
            # membership name and member-stock leader remain usable when that quote fails.
            if not sector.get("outer_precise", sector.get("precise")):
                invalid_fields[4].add("sector_change")

        if index_issues:
            invalid_fields[6].update(required[6])

        checks = []
        for line_number in range(1, 7):
            manual_keys = [key for key in required[line_number] if key in manual_data]
            unresolved_fields = [
                key for key in required[line_number]
                if key in invalid_fields[line_number] and key not in manual_data
            ]
            hard_missing_identity = line_number in {1, 2} and not stock.get("code")
            passed = not hard_missing_identity and not unresolved_fields
            status = "manual" if passed and manual_keys else "passed" if passed else "failed"
            reasons = [] if passed else [
                *( ["请先输入并载入股票代码或名称"] if hard_missing_identity else automatic_issues[line_number] ),
                *( ["需补充：" + "、".join(schema[key]["label"] for key in unresolved_fields)] if unresolved_fields else [] ),
            ]
            score = float(scores[line_number - 1]["score"])
            position, layer, formula = names[line_number]
            checks.append({
                "line": line_number, "position": position, "layer": layer, "formula": formula,
                "status": status, "passed": passed, "reasons": reasons,
                "score": round(score, 3) if passed else None,
                "line_value": _score_to_line(score) if passed else None,
                "evidence": scores[line_number - 1]["evidence"] if passed else [],
                "fields": [
                    {
                        "key": key, "label": schema[key]["label"], "unit": schema[key].get("unit", ""),
                        "type": schema[key].get("type", "number"), "options": schema[key].get("options", []),
                        "value": value_map.get(key), "manual": key in manual_data,
                        "required": True, "min": schema[key].get("min"), "max": schema[key].get("max"),
                        "integer": bool(schema[key].get("integer")),
                    }
                    for key in required[line_number]
                ],
            })
        return checks
