from __future__ import annotations

from copy import deepcopy
from pathlib import Path
from typing import Any

from backend.features.heaven.hexagram import LINE_POSITIONS, from_lines, line_for_score

LINE_META = (
    ("地", "内", "个股内核"),
    ("地", "外", "个股外显"),
    ("人", "内", "行业内核"),
    ("人", "外", "行业外显"),
    ("天", "内", "市场内核"),
    ("天", "外", "指数外显"),
)
MANUAL_FIELDS = {
    "sector.name",
    "sector.change",
    "sector.up_count",
    "sector.down_count",
    "sector.member_count",
    "sector.quoted_count",
    "sector.coverage",
    "sector.member_equal_change",
    "sector.relative_turnover",
    "sector.leader",
    "sector.leading_pct",
}


class TrendDataError(RuntimeError):
    def __init__(self, checks: list[dict[str, Any]], payload: dict[str, Any]) -> None:
        super().__init__("六爻量化数据未全部通过安全门，暂不成卦。")
        self.checks = checks
        self.payload = payload


def calculate(
    payload: dict[str, Any],
    data_path: Path,
    manual: dict[str, Any] | None = None,
) -> dict[str, Any]:
    normalized, manual_paths = apply_manual(payload, manual or {})
    checks = validate(normalized, manual_paths)
    if any(not item["passed"] for item in checks):
        raise TrendDataError(checks, normalized)
    scores = _scores(normalized)
    values = [line_for_score(item["score"]) for item in scores]
    hexagram = from_lines(values, data_path)
    for index, line in enumerate(hexagram["lines"]):
        talent, layer, role = LINE_META[index]
        line.update(
            talent=talent,
            layer=layer,
            role=role,
            score=round(scores[index]["score"], 4),
            evidence=scores[index]["evidence"],
            validation=checks[index],
        )
    average = sum(item["score"] for item in scores) / 6
    moving_names = [LINE_POSITIONS[index - 1] for index in hexagram["moving_lines"]]
    return {
        "trade_date": normalized["trade_date"],
        "stock": normalized["stock"],
        "sector": normalized["sector"],
        "hexagram": hexagram,
        "movement": {
            "moving_names": moving_names,
            "label": (
                f"{'、'.join(moving_names)}动，{hexagram['name']}之{hexagram['transformed']['name']}"
                if moving_names
                else f"无动爻，守{hexagram['name']}本势"
            ),
        },
        "momentum_score": round(average * 100),
        "momentum_label": _momentum_label(average),
        "checks": checks,
        "manual_fields": sorted(manual_paths),
        "notice": "卦象来自客观行情的固定量化映射，仅供传统文化与娱乐化观察。",
    }


def apply_manual(
    payload: dict[str, Any], manual: dict[str, Any]
) -> tuple[dict[str, Any], set[str]]:
    result = deepcopy(payload)
    failed_paths = _failed_paths(result)
    applied: set[str] = set()
    for path, value in _flatten(manual).items():
        if path not in MANUAL_FIELDS or path not in failed_paths or value in (None, ""):
            continue
        section, key = path.split(".", 1)
        result.setdefault(section, {})[key] = value
        applied.add(path)
    return result, applied


def validate(payload: dict[str, Any], manual_paths: set[str] | None = None) -> list[dict[str, Any]]:
    manual_paths = manual_paths or set()
    trade_date = str(payload.get("trade_date") or "")
    stock = payload.get("stock") or {}
    sector = payload.get("sector") or {}
    market = payload.get("market") or {}
    indexes = payload.get("indices") or []
    mode = str(payload.get("mode") or "historical")
    stock_fields = (
        (
            "change",
            "amount_percentile",
            "turnover_rate",
            "turnover_relative",
            "volume_activity_ratio",
        )
        if mode == "intraday"
        else ("change", "amount_percentile", "turnover_rate")
    )
    stock_ok = (
        str(stock.get("trade_date") or "") == trade_date
        and str(stock.get("quote_kind") or "") == ("realtime" if mode == "intraday" else "daily")
        and _has(stock, *stock_fields)
    )
    sector_common = (
        str(sector.get("trade_date") or trade_date) == trade_date
        and str(sector.get("taxonomy") or "") == "申万二级"
        and _has(
            sector,
            "change",
            "up_count",
            "down_count",
            "member_count",
            "quoted_count",
            "coverage",
            "leading_pct",
        )
    )
    member_count = _number(sector.get("member_count"))
    quoted_count = _number(sector.get("quoted_count"))
    coverage = _number(sector.get("coverage"))
    complete_members = member_count > 0 and quoted_count == member_count and coverage >= 0.98
    sufficient_members = (
        member_count > 0 and coverage >= 0.98 and quoted_count >= member_count * 0.98
    )
    sector_mode = str(sector.get("quote_kind") or "") == (
        "realtime" if mode == "intraday" else "daily"
    )
    if mode == "intraday":
        sector_inner = sector_common and sector_mode and _has(sector, "relative_turnover")
    else:
        sector_inner = sector_common and sector_mode and _has(sector, "member_equal_change")
    sector_inner = sector_inner and (complete_members or sufficient_members)
    sector_outer = sector_common and sector_mode and bool(str(sector.get("name") or ""))
    market_ok = (
        str(market.get("trade_date") or "") == trade_date
        and str(market.get("quote_kind") or "") == ("realtime" if mode == "intraday" else "daily")
        and _has(
            market,
            "sentiment_score",
            "seal_rate",
            "amount_billion",
            "average_amount_billion",
            "up_count",
            "down_count",
            "limit_up_count",
            "limit_down_count",
        )
    )
    expected = {"000001.SH", "399001.SZ", "399006.SZ"}
    present = {
        str(item.get("identifier") or "")
        for item in indexes
        if str(item.get("trade_date") or "") == trade_date
        and str(item.get("quote_kind") or "") == ("realtime" if mode == "intraday" else "daily")
        and item.get("change") is not None
    }
    details = (
        (stock_ok, "个股交易日、行情类型及成交活跃数据有效", {"stock"}),
        (stock_ok and _has(stock, "streak", "status"), "个股涨跌、连板和事件状态有效", {"stock"}),
        (sector_inner, f"申万二级行业有效成分 {int(quoted_count)}/{int(member_count)}", {"sector"}),
        (sector_outer, "申万二级行业及领涨股涨跌有效", {"sector"}),
        (market_ok, "市场情绪、封板、成交、宽度和涨跌停结构有效", {"market"}),
        (present == expected, "上证、深证、创业板三条指数行情完整", {"indices"}),
    )
    checks = []
    for index, (passed, message, sections) in enumerate(details):
        used_manual = any(path.split(".", 1)[0] in sections for path in manual_paths)
        checks.append(
            {
                "position": index + 1,
                "position_name": LINE_POSITIONS[index],
                "role": LINE_META[index][2],
                "passed": bool(passed),
                "source": "manual" if used_manual else "automatic",
                "message": message if passed else _failure_message(index, payload),
            }
        )
    return checks


def _scores(payload: dict[str, Any]) -> list[dict[str, Any]]:
    stock = payload["stock"]
    sector = payload["sector"]
    market = payload["market"]
    mode = payload.get("mode") or "historical"
    amount = _clamp(_number(stock["amount_percentile"]) / 100, 0, 1)
    if mode == "intraday":
        relative_turnover = _clamp((_number(stock["turnover_relative"]) - 1) / 1.5)
        activity = _clamp((_number(stock["volume_activity_ratio"]) - 1) / 1.5)
        stock_inner = (amount * 2 - 1) * 0.35 + relative_turnover * 0.35 + activity * 0.30
    else:
        turnover = _clamp(_number(stock["turnover_rate"]) / 20, 0, 1)
        seal = _clamp(_number(stock.get("seal_amount_million")) / 15000, 0, 1)
        stability = 1 - _clamp(_number(stock.get("open_times")) / 6, 0, 1)
        stock_inner = (amount * 0.32 + turnover * 0.22 + seal * 0.25 + stability * 0.21) * 2 - 1
    adjustment = -0.7 if stock["status"] == "跌停" else -0.25 if stock["status"] == "炸板" else 0.15
    stock_outer = _clamp(
        _clamp(_number(stock["change"]) / 10) * 0.7
        + _clamp(_number(stock["streak"]) / 5, 0, 1) * 0.2
        + adjustment
    )
    up = _number(sector["up_count"])
    down = _number(sector["down_count"])
    breadth = _clamp((up - down) / max(up + down, 1))
    leader = _clamp(_number(sector["leading_pct"]) / 10)
    if mode == "intraday":
        relative = _clamp((_number(sector["relative_turnover"]) - 1) / 1.5)
        sector_inner = breadth * 0.6 + relative * 0.4
    else:
        equal_change = _clamp(_number(sector["member_equal_change"]) / 5)
        sector_inner = breadth * 0.6 + equal_change * 0.35 + leader * 0.05
    sector_outer = _clamp(_number(sector["change"]) / 5) * 0.9 + leader * 0.1
    sentiment = _clamp(_number(market["sentiment_score"]) / 100, 0, 1) * 2 - 1
    seal_rate = _clamp(_number(market["seal_rate"]) / 100, 0, 1) * 2 - 1
    amount_change = _clamp(
        (_number(market["amount_billion"]) / max(_number(market["average_amount_billion"]), 1) - 1)
        * 3
    )
    market_up, market_down = _number(market["up_count"]), _number(market["down_count"])
    market_breadth = _clamp((market_up / max(market_up + market_down, 1) - 0.5) * 2)
    limit_up = _number(market["limit_up_count"])
    limit_down = _number(market["limit_down_count"])
    limit_balance = _clamp((limit_up - limit_down) / max(limit_up + limit_down, 1))
    market_inner = (
        sentiment * 0.35
        + seal_rate * 0.20
        + amount_change * 0.20
        + market_breadth * 0.15
        + limit_balance * 0.10
    )
    index_change = sum(_number(item["change"]) for item in payload["indices"]) / 3
    return [
        {
            "score": _clamp(stock_inner),
            "evidence": (
                [
                    f"成交额分位 {amount * 100:.0f}%",
                    f"相对换手 {_number(stock['turnover_relative']):.2f}",
                    f"同进度量能 {_number(stock['volume_activity_ratio']):.2f}",
                ]
                if mode == "intraday"
                else [
                    f"成交额分位 {amount * 100:.0f}%",
                    f"换手率 {_number(stock['turnover_rate']):.2f}%",
                ]
            ),
        },
        {
            "score": _clamp(stock_outer),
            "evidence": [
                f"涨跌 {_number(stock['change']):+.2f}%",
                f"状态 {stock['status'] or '普通'}",
            ],
        },
        {
            "score": _clamp(sector_inner),
            "evidence": [
                f"上涨 {int(up)} / 下跌 {int(down)}",
                "有效成分 "
                f"{int(_number(sector['quoted_count']))}/"
                f"{int(_number(sector['member_count']))}",
            ],
        },
        {
            "score": _clamp(sector_outer),
            "evidence": [
                f"行业涨跌 {_number(sector['change']):+.2f}%",
                f"领涨股 {_number(sector['leading_pct']):+.2f}%",
            ],
        },
        {
            "score": _clamp(market_inner),
            "evidence": [
                f"情绪 {_number(market['sentiment_score']):.0f}",
                f"封板率 {_number(market['seal_rate']):.1f}%",
            ],
        },
        {"score": _clamp(index_change / 3), "evidence": [f"三大指数平均 {index_change:+.2f}%"]},
    ]


def _failed_paths(payload: dict[str, Any]) -> set[str]:
    sector = payload.get("sector") or {}
    return {path for path in MANUAL_FIELDS if sector.get(path.split(".", 1)[1]) in (None, "")}


def _flatten(value: dict[str, Any], prefix: str = "") -> dict[str, Any]:
    result: dict[str, Any] = {}
    for key, item in value.items():
        path = f"{prefix}.{key}" if prefix else key
        if isinstance(item, dict):
            result.update(_flatten(item, path))
        else:
            result[path] = item
    return result


def _has(value: dict[str, Any], *keys: str) -> bool:
    return all(key in value and value[key] is not None and value[key] != "" for key in keys)


def _failure_message(index: int, payload: dict[str, Any]) -> str:
    messages = (
        "个股成交活跃或交易日期数据缺失",
        "个股涨跌、连板或事件状态缺失",
        "申万二级行业成分宽度、覆盖率或换手数据缺失",
        "申万二级行业涨跌或领涨股涨跌缺失",
        "市场情绪、成交或宽度数据缺失",
        "指数层缺少三大指数的有效行情",
    )
    return messages[index]


def _clamp(value: float, lower: float = -1, upper: float = 1) -> float:
    return max(lower, min(upper, value))


def _number(value: Any) -> float:
    try:
        return float(value)
    except (TypeError, ValueError):
        return 0.0


def _momentum_label(score: float) -> str:
    if score >= 0.45:
        return "势盛而动"
    if score >= 0.12:
        return "势起未极"
    if score > -0.12:
        return "阴阳相持"
    if score > -0.45:
        return "势弱宜察"
    return "势衰宜守"
