"""Post-publish revision review for datasets whose upstream may rewrite T-day fields.

HEL-423 field evidence, not a whitelist of tolerated diffs:

- 2026-09-07 valuation/daily_basic: hub published 003021.SZ turnover_rate=1.3565
  at 17:10; website legacy and a direct Tushare read at 21:05 both showed 1.3572.
  The other seven observed objects (daily, moneyflow, auction, stocks, status,
  index_daily, calendar) matched. Hub had already stopped the day after the
  first successful publish, so the revision never self-healed.
- 2026-09-02: same dataset, opposite direction (hub already held the later
  value). Confirms daily_basic is rewritten after the first complete dump.

Daily bars, moneyflow, auction and index_daily have no same-evening field
revision evidence. Stocks already refreshes at 20:00/23:10. Review therefore
fetches only configured revision-risk datasets (default: valuation) and
compares the website-requested field set. No numeric tolerance.
"""

from __future__ import annotations

from typing import Any

from datahub.db import DATASET_TABLES
from datahub.normalize import VALUATION_FIELDS
from datahub.numbers import finite_number, round4

# Datasets with proven same-evening upstream rewrites. Config may replace this
# list; it must not silently expand to a full EOD re-pull.
DEFAULT_REVISION_DATASETS = ("valuation",)

# Website daily_basic request (HEL-423): ts_code/trade_date plus the eight
# value fields used by the old link and field_gates.
WEBSITE_COMPARE_FIELDS: dict[str, tuple[str, ...]] = {
    "valuation": VALUATION_FIELDS,
}

REVISION_STATES = ("waiting_review", "review_failed", "aligned", "cutoff")


def revision_datasets(quality: dict[str, Any] | None) -> tuple[str, ...]:
    raw = (quality or {}).get("revision_review_datasets")
    if isinstance(raw, (list, tuple)) and raw:
        names = tuple(str(item) for item in raw if str(item))
        if names:
            return names
    return DEFAULT_REVISION_DATASETS


def compare_fields(dataset: str) -> tuple[str, ...]:
    fields = WEBSITE_COMPARE_FIELDS.get(dataset)
    if fields:
        return fields
    gate = {}
    return tuple(str(item) for item in (gate.get("fields") or []) if str(item))


def _norm_value(field: str, value: Any) -> Any:
    if field in {"ts_code", "trade_date"}:
        return str(value or "")
    number = round4(finite_number(value))
    return number


def row_signature(row: dict[str, Any], fields: tuple[str, ...]) -> tuple[Any, ...]:
    return tuple(_norm_value(field, row.get(field)) for field in fields)


def diff_published_vs_upstream(
    dataset: str,
    published: list[dict[str, Any]],
    upstream: list[dict[str, Any]],
    *,
    max_diffs: int = 20,
) -> dict[str, Any]:
    """Exact compare on website-requested fields. No tolerance / exemption."""
    fields = compare_fields(dataset)
    if not fields:
        fields = tuple(sorted({key for row in published + upstream for key in row if key != "batch_id"}))
    pub_map = {str(row.get("ts_code") or "").upper(): row for row in published}
    up_map = {str(row.get("ts_code") or "").upper(): row for row in upstream}
    missing = sorted(code for code in pub_map if code not in up_map)
    extra = sorted(code for code in up_map if code not in pub_map)
    diffs: list[dict[str, Any]] = []
    for code in sorted(set(pub_map) & set(up_map)):
        left = row_signature(pub_map[code], fields)
        right = row_signature(up_map[code], fields)
        if left == right:
            continue
        for field, old, new in zip(fields, left, right):
            if old == new:
                continue
            diffs.append({"ts_code": code, "field": field, "published": old, "upstream": new})
            if len(diffs) >= max_diffs:
                break
        if len(diffs) >= max_diffs:
            break
    changed = bool(diffs or missing or extra)
    return {
        "changed": changed,
        "dataset": dataset,
        "fields": list(fields),
        "published_rows": len(published),
        "upstream_rows": len(upstream),
        "missing_codes": missing[:max_diffs],
        "extra_codes": extra[:max_diffs],
        "diffs": diffs,
    }


def official_table(dataset: str) -> str:
    return DATASET_TABLES[dataset][0]
