from __future__ import annotations

import json
from statistics import median
from typing import Any


def build_auction(
    *,
    trade_date: str,
    raw_rows: tuple[dict[str, Any], ...],
    price_limits: tuple[dict[str, Any], ...],
    directory: dict[str, dict[str, Any]],
    prior_snapshot: dict[str, Any],
    ths_hot: tuple[dict[str, Any], ...],
    dc_hot: tuple[dict[str, Any], ...],
    history: list[dict[str, Any]],
    dynamic: bool,
) -> dict[str, Any]:
    rows = _normalize_rows(raw_rows, price_limits, directory, dynamic)
    candidates, focus_rows = _score_candidates(rows, prior_snapshot, ths_hot, dc_hot)
    scored = {str(item["code"]): item for item in candidates}
    candidate_codes = {str(item["code"]) for item in candidates}
    one_price_rows = [
        {
            **item,
            **scored.get(str(item["code"]), {}),
            "attention_score": None,
            "expectation": "",
            "expected_change": None,
            "expectation_reason": "竞价价格封于当日涨停价，已从普通异动评分中隔离",
        }
        for item in rows
        if item["is_one_price"]
    ]
    one_price_codes = {str(item["code"]) for item in one_price_rows}
    candidates = [item for item in candidates if item["code"] not in one_price_codes]
    focus_rows = [item for item in focus_rows if item["code"] not in one_price_codes]
    one_price_rows.sort(
        key=lambda item: (
            bool(item.get("is_market_core")),
            _number(item.get("prior_streak")),
            _number(item.get("amount_million")),
        ),
        reverse=True,
    )
    changes = [float(item["change"]) for item in rows]
    amount_billion = round(sum(float(item["amount_million"]) for item in rows) / 100, 2)
    amount_history = [item for item in history if item.get("trade_date") != trade_date][-9:]
    amount_history.append(
        {"trade_date": trade_date, "amount_billion": amount_billion, "stock_count": len(rows)}
    )
    prior_amounts = [float(item["amount_billion"]) for item in amount_history[:-1]]
    previous_amount = prior_amounts[-1] if prior_amounts else 0
    five_day = prior_amounts[-5:]
    five_day_average = sum(five_day) / len(five_day) if five_day else 0
    eligible = sum(
        bool(item.get("identifier"))
        and not str(item.get("name") or "").upper().startswith(("N", "C"))
        for item in directory.values()
    )
    coverage = min(len(rows) / max(eligible, 1), 1)
    expectations = {
        label: sum(item.get("expectation") == label for item in candidates)
        for label in ("超预期", "符合预期", "低于预期")
    }
    return {
        "trade_date": trade_date,
        "dynamic": dynamic,
        "coverage": round(coverage, 4),
        "summary": {
            "stock_count": len(rows),
            "candidate_count": len(candidates),
            "focus_count": len(focus_rows),
            "one_price_count": len(one_price_rows),
            "amount_billion": amount_billion,
            "amount_change_previous": (
                round((amount_billion / previous_amount - 1) * 100, 1)
                if previous_amount
                else None
            ),
            "amount_change_5d": (
                round((amount_billion / five_day_average - 1) * 100, 1)
                if five_day_average
                else None
            ),
            "median_change": round(median(changes), 2) if changes else None,
        },
        "expectations": expectations,
        "themes": _theme_evidence(prior_snapshot, candidates + one_price_rows),
        "amount_history": amount_history,
        "focus_rows": focus_rows,
        "one_price_rows": one_price_rows,
        "_market_rows": rows,
        "rows": candidates,
        "all_market_count": len(rows),
        "candidate_market_count": len(candidate_codes),
    }


def build_watchlist_rows(
    market_rows: list[dict[str, Any]],
    candidates: list[dict[str, Any]],
    one_price_rows: list[dict[str, Any]],
    watchlist: tuple[dict[str, Any], ...],
) -> list[dict[str, Any]]:
    market = {str(item["identifier"]): item for item in market_rows}
    enriched = {
        str(item["identifier"]): item for item in candidates + one_price_rows
    }
    result = []
    for saved in watchlist:
        identifier = str(saved.get("identifier") or "")
        row = enriched.get(identifier)
        if row:
            result.append({**row, "is_watchlist": True, "available": True})
            continue
        raw = market.get(identifier)
        if raw:
            actual = _number(raw.get("change")) + _confirmation(raw)
            item = {
                **raw,
                "candidate_sources": ["我的自选"],
                "source_label": "我的自选",
                "prior_streak": 0,
                "concepts": [],
                "expected_change": 0.0,
                "actual_strength": round(actual, 2),
                "expectation": _expectation(actual, 0),
                "core_tags": [],
                "is_market_core": False,
                "is_watchlist": True,
                "available": True,
            }
            item["attention_score"] = _attention(item, 0, False, False)
            item["expectation_reason"] = "自选观察，按当日竞价强度与成交确认评估"
            result.append(item)
            continue
        result.append(
            {
                "identifier": identifier,
                "code": identifier.split(".")[0],
                "name": str(saved.get("name") or ""),
                "sector": str(saved.get("sector") or ""),
                "is_watchlist": True,
                "available": False,
            }
        )
    return sorted(
        result,
        key=lambda item: (
            bool(item.get("available")),
            _number(item.get("attention_score")),
        ),
        reverse=True,
    )


def _normalize_rows(
    raw_rows: tuple[dict[str, Any], ...],
    price_limits: tuple[dict[str, Any], ...],
    directory: dict[str, dict[str, Any]],
    dynamic: bool,
) -> list[dict[str, Any]]:
    limits = {str(row.get("ts_code") or ""): row for row in price_limits}
    latest: dict[str, dict[str, Any]] = {}
    for raw in raw_rows:
        identifier = str(raw.get("thscode") or raw.get("ts_code") or "").upper()
        if not identifier or identifier not in directory:
            continue
        previous = latest.get(identifier)
        if previous is None or str(raw.get("time") or "") >= str(previous.get("time") or ""):
            latest[identifier] = raw
    rows = []
    for identifier, raw in latest.items():
        stock = directory[identifier]
        price = _number(raw.get("latest" if dynamic else "price"))
        pre_close = _number(raw.get("preClose" if dynamic else "pre_close"))
        volume = _number(raw.get("volume" if dynamic else "vol"))
        amount = _number(raw.get("amount"))
        if amount <= 0 and price > 0 and volume > 0:
            amount = price * volume
        if price <= 0 or pre_close <= 0:
            continue
        change = (price / pre_close - 1) * 100
        up_limit = _number((limits.get(identifier) or {}).get("up_limit"))
        rows.append(
            {
                "identifier": identifier,
                "code": str(stock.get("code") or identifier.split(".")[0]),
                "name": str(stock.get("name") or ""),
                "sector": str(stock.get("sector") or "其他"),
                "price": round(price, 2),
                "change": round(change, 2),
                "amount_million": round(amount / 1_000_000, 2),
                "turnover_rate": round(
                    _number(raw.get("turnoverRatio" if dynamic else "turnover_rate")), 4
                ),
                "volume_ratio": round(
                    _number(raw.get("volumeRatio" if dynamic else "volume_ratio")), 2
                ),
                "up_limit": round(up_limit, 2) if up_limit else None,
                "is_one_price": bool(
                    up_limit > 0 and abs(price - up_limit) <= max(0.001, up_limit * 0.00005)
                ),
                "snapshot_time": str(raw.get("time") or ""),
            }
        )
    rows.sort(
        key=lambda item: (float(item["amount_million"]), float(item["volume_ratio"])),
        reverse=True,
    )
    return rows


def _score_candidates(
    rows: list[dict[str, Any]],
    prior_snapshot: dict[str, Any],
    ths_hot: tuple[dict[str, Any], ...],
    dc_hot: tuple[dict[str, Any], ...],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    prior_limits = list(prior_snapshot.get("limits") or [])
    prior_broken = list(prior_snapshot.get("broken") or [])
    prior_sectors = list(prior_snapshot.get("sectors") or [])
    strong_sectors = {str(item.get("name") or "") for item in prior_sectors[:5]}
    identities: dict[str, dict[str, Any]] = {}
    core_tags: dict[str, set[str]] = {}

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

    highest = max((int(_number(item.get("streak"), 1)) for item in prior_limits), default=0)
    for item in prior_limits:
        entry = ensure(item)
        if not entry:
            continue
        code, identity = entry
        streak = max(1, int(_number(item.get("streak"), 1)))
        identity["streak"] = streak
        identity["sources"].append("昨日涨停")
        if streak >= 3:
            core_tags.setdefault(code, set()).add("三板以上")
        if highest and streak == highest:
            core_tags.setdefault(code, set()).add("市场最高板")
    for item in prior_broken:
        entry = ensure(item)
        if entry and "昨日炸板" not in entry[1]["sources"]:
            entry[1]["sources"].append("昨日炸板")
    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 members:
            leader = max(
                members,
                key=lambda item: (
                    int(_number(item.get("streak"), 1)),
                    _number(item.get("amount")),
                ),
            )
            core_tags.setdefault(str(leader.get("code") or ""), set()).add("题材核心")
    if prior_limits:
        leader = max(
            prior_limits,
            key=lambda item: (
                int(_number(item.get("streak"), 1)),
                str(item.get("sector") or "") in strong_sectors,
                _number(item.get("amount")),
            ),
        )
        core_tags.setdefault(str(leader.get("code") or ""), set()).add("市场领涨")

    hot_records: dict[str, dict[str, Any]] = {}
    for rows_source, source, expected_type, rank_key in (
        (ths_hot, "同花顺热榜", "热股", "ths_rank"),
        (dc_hot, "东方财富热榜", "A股市场", "dc_rank"),
    ):
        for item in rows_source:
            if str(item.get("data_type") or "") != expected_type:
                continue
            code = str(item.get("ts_code") or "").split(".")[0]
            rank = max(1, int(_number(item.get("rank"), 9999)))
            if not code or rank > 20:
                continue
            hot = hot_records.setdefault(
                code, {"ths_rank": None, "dc_rank": None, "concepts": []}
            )
            hot[rank_key] = rank
            if rank_key == "ths_rank":
                hot["concepts"] = _concepts(item.get("concept"))
            identity = identities.setdefault(
                code,
                {
                    "sources": [],
                    "streak": 0,
                    "sector": "其他",
                    "concepts": [],
                    "ths_rank": None,
                    "dc_rank": None,
                },
            )
            identity[rank_key] = rank
            identity["concepts"] = hot["concepts"] or identity["concepts"]
            if source not in identity["sources"]:
                identity["sources"].append(source)
    hot_ranked = sorted(
        hot_records,
        key=lambda code: (
            (21 - (hot_records[code]["ths_rank"] or 21)) * 0.5
            + (21 - (hot_records[code]["dc_rank"] or 21)) * 0.25
            + (10 if hot_records[code]["ths_rank"] and hot_records[code]["dc_rank"] else 0)
        ),
        reverse=True,
    )
    for code in hot_ranked[:5]:
        core_tags.setdefault(code, set()).add("人气前5")

    normalized = []
    for row in rows:
        identity = identities.get(str(row["code"]))
        if not identity:
            continue
        ranks = [
            rank
            for rank in (identity.get("ths_rank"), identity.get("dc_rank"))
            if isinstance(rank, int)
        ]
        if ranks and min(ranks) > 10 and len(ranks) == 1 and row["code"] not in core_tags:
            if not any(source in {"昨日涨停", "昨日炸板"} for source in identity["sources"]):
                continue
        streak = int(identity["streak"])
        expected = {0: 0.5, 1: 1.5, 2: 3.0, 3: 4.0}.get(streak, 5.0)
        expected += 0.8 if len(ranks) == 2 else 0.7 if ranks and min(ranks) <= 10 else 0
        expected = min(expected, 6.5)
        confirmation = _confirmation(row)
        actual_strength = float(row["change"]) + confirmation
        expectation = _expectation(actual_strength, expected)
        tags = sorted(core_tags.get(str(row["code"]), set()))
        scored = {
            **row,
            "sector": identity["sector"] if identity["sector"] != "其他" else row["sector"],
            "candidate_sources": identity["sources"],
            "source_label": " · ".join(identity["sources"]),
            "prior_streak": streak,
            "concepts": identity["concepts"],
            "expected_change": round(expected, 2),
            "actual_strength": round(actual_strength, 2),
            "expectation": expectation,
            "core_tags": tags,
            "is_market_core": bool(tags),
        }
        scored["attention_score"] = _attention(
            scored,
            expected,
            bool(tags),
            str(scored["sector"]) in strong_sectors,
        )
        scored["expectation_reason"] = _reason(scored)
        normalized.append(scored)
    normalized.sort(
        key=lambda item: (float(item["attention_score"]), float(item["amount_million"])),
        reverse=True,
    )
    matched = {
        str(item["code"])
        for item in [row for row in normalized if row["expectation"] == "符合预期"][:20]
    }
    mandatory = [item for item in normalized if item["is_market_core"]]
    optional = [
        item
        for item in normalized
        if not item["is_market_core"]
        and (
            (item["attention_score"] >= 55 and item["expectation"] != "符合预期")
            or item["code"] in matched
        )
    ]
    focus = mandatory + optional[: max(0, 30 - len(mandatory))]
    focus.sort(key=lambda item: float(item["attention_score"]), reverse=True)
    return normalized, focus


def _confirmation(row: dict[str, Any]) -> float:
    volume_ratio = _number(row.get("volume_ratio"))
    turnover = _number(row.get("turnover_rate"))
    amount = _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 >= 0.15 else -0.25 if turnover < 0.03 else 0)
        + (0.3 if amount >= 20 else 0.15 if amount >= 5 else -0.3 if amount < 1 else 0)
    )


def _attention(
    row: dict[str, Any], expected: float, core: bool, strong_sector: bool
) -> float:
    sources = list(row.get("candidate_sources") or [])
    streak = int(row.get("prior_streak") or 0)
    identity = 35 if core else 27 if streak >= 2 else 21 if sources else 14
    deviation = min(30, abs(_number(row.get("change")) - expected) * 5)
    volume = min(10, max(0, _number(row.get("volume_ratio"))) / 2 * 10)
    amount = min(6, max(0, _number(row.get("amount_million"))) / 10 * 6)
    turnover = min(4, max(0, _number(row.get("turnover_rate"))) / 0.2 * 4)
    theme = 15 if strong_sector else 7 if row.get("concepts") else 0
    return round(min(100, identity + deviation + volume + amount + turnover + theme), 1)


def _expectation(actual: float, expected: float) -> str:
    difference = actual - expected
    return "超预期" if difference >= 1.5 else "低于预期" if difference <= -1.5 else "符合预期"


def _reason(row: dict[str, Any]) -> str:
    streak = int(row.get("prior_streak") or 0)
    identity = f"昨日{streak}板" if streak > 1 else "昨日首板" if streak else "热榜标的"
    difference = _number(row.get("change")) - _number(row.get("expected_change"))
    direction = "高于" if difference > 0 else "低于" if difference < 0 else "贴合"
    return f"{identity}，竞价涨幅{direction}预期{abs(difference):.1f}个百分点"


def _theme_evidence(
    prior_snapshot: dict[str, Any], 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 "其他")
        members = [row for row in rows if str(row.get("sector") or "其他") == name]
        changes = [_number(row.get("change")) for row in members]
        middle = median(changes) if changes else None
        positive = sum(value > 0.2 for value in changes) / len(changes) * 100 if changes else 0
        status = (
            "强承接" if middle is not None and middle >= 2 and positive >= 60
            else "有承接" if middle is not None and middle >= 0 and positive >= 50
            else "分歧" if middle is not None and middle > -2
            else "承接弱"
        )
        carry.append(
            {
                "name": name,
                "status": status,
                "prior_limit_count": int(_number(sector.get("count"))),
                "matched_count": len(members),
                "median_change": round(middle, 2) if middle is not None else None,
                "positive_rate": round(positive, 1),
            }
        )
    prior_names = {str(item.get("name") or "") for item in prior_sectors}
    groups: dict[str, dict[str, dict[str, Any]]] = {}
    for row in rows:
        for concept in row.get("concepts") or []:
            if concept and concept not in prior_names:
                groups.setdefault(str(concept), {})[str(row["code"])] = row
    new_themes = []
    for name, mapped in groups.items():
        members = list(mapped.values())
        changes = [_number(item.get("change")) for item in members]
        positive_rate = sum(value > 0.2 for value in changes) / len(changes)
        if len(members) >= 2 and median(changes) >= 2 and positive_rate >= 0.67:
            new_themes.append(
                {
                    "name": name,
                    "stock_count": len(members),
                    "median_change": round(median(changes), 2),
                    "leaders": [
                        str(item.get("name") or "")
                        for item in sorted(
                            members,
                            key=lambda item: _number(item.get("change")),
                            reverse=True,
                        )[:3]
                    ],
                }
            )
    new_themes.sort(key=lambda item: (item["stock_count"], item["median_change"]), reverse=True)
    return {"carry": carry, "new_themes": new_themes[:8]}


def _concepts(value: Any) -> list[str]:
    if isinstance(value, list):
        return [str(item).strip() for item in value if str(item).strip()]
    text = str(value or "").strip()
    if not text:
        return []
    try:
        parsed = json.loads(text)
        if isinstance(parsed, list):
            return [str(item).strip() for item in parsed if str(item).strip()]
    except json.JSONDecodeError:
        pass
    return [part.strip() for part in text.replace("，", ",").split(",") if part.strip()]


def _number(value: Any, default: float = 0.0) -> float:
    try:
        number = float(value)
        return number if number == number else default
    except (TypeError, ValueError):
        return default
