"""Validate and render the Figure 3 literature and point registry."""

from __future__ import annotations

import html
import json
import math
from pathlib import Path
from typing import Any

import pandas as pd

ROOT = Path(__file__).resolve().parent
PAPER_ROOT = ROOT.parent / "paper_apj_v19_cgmsum_draft"
REFERENCE_REGISTRY = ROOT / "figure3_reference_registry.json"
CANONICAL_URL = "https://m31cgm-mw-m31-profile-explorer.pages.dev/"
EXPECTED_REFERENCE_STUDIES = {
    "henley-shelton-2013",
    "ponti-efeds-2023",
    "ueda-2022",
    "kaaret-halosat-2020",
    "locatelli-2024",
    "zhang-2024",
    "grayson-2025",
}


def validate_reference_registry(
    registry: dict[str, Any], ledger_path: Path | None = None
) -> None:
    """Fail closed when literature prose and the Figure 3 numeric ledger diverge."""
    ledger_path = ledger_path or PAPER_ROOT / "m31_cgmsum_conditional_prior_ledger.csv"
    ledger = pd.read_csv(ledger_path)
    studies = registry.get("studies")
    points = registry.get("points")
    if not isinstance(studies, list) or not isinstance(points, list):
        raise ValueError("reference registry requires study and point inventories")

    study_ids = [study.get("study_id") for study in studies]
    point_ids = [point.get("prior_id") for point in points]
    expected_points = set(ledger["prior_id"])
    if set(point_ids) != expected_points or len(point_ids) != len(set(point_ids)):
        raise ValueError(
            "reference point inventory does not match Figure 3 ledger: "
            f"missing={sorted(expected_points - set(point_ids))}, "
            f"extra={sorted(set(point_ids) - expected_points)}"
        )
    if set(study_ids) != EXPECTED_REFERENCE_STUDIES or len(study_ids) != len(
        set(study_ids)
    ):
        raise ValueError("reference study inventory does not match the seven Figure 3 papers")

    study_map = {study["study_id"]: study for study in studies}
    if len({study.get("slug") for study in studies}) != len(studies):
        raise ValueError("study slugs must be unique")
    if len({point.get("slug") for point in points}) != len(points):
        raise ValueError("point slugs must be unique")

    bilingual_fields = {
        "overview",
        "instrument",
        "sightlines",
        "measurement",
        "model",
        "published_results",
        "transfer_to_m31",
        "limitations",
    }
    for study in studies:
        if study.get("kind") not in {
            "mw-observation",
            "external-galaxy-stack",
            "simulation",
        }:
            raise ValueError(f"study {study['study_id']} has an invalid kind")
        missing = bilingual_fields - set(study)
        if missing:
            raise ValueError(f"study {study['study_id']} missing fields: {sorted(missing)}")
        for field in bilingual_fields:
            value = study[field]
            if not isinstance(value, dict) or set(value) != {"zh", "en"} or not all(
                isinstance(value[language], str) and value[language].strip()
                for language in ("zh", "en")
            ):
                raise ValueError(f"study {study['study_id']} field {field} is not bilingual")
        citation = study.get("citation", {})
        urls = citation.get("primary_urls", {}) if isinstance(citation, dict) else {}
        if not citation.get("doi", "").startswith("10.") or not urls:
            raise ValueError(f"study {study['study_id']} lacks DOI or primary URLs")
        if not all(isinstance(url, str) and url.startswith("https://") for url in urls.values()):
            raise ValueError(f"study {study['study_id']} has a non-HTTPS primary URL")
        snapshot = citation.get("source_snapshot", {})
        sha256 = str(snapshot.get("sha256", ""))
        if (
            not str(snapshot.get("version", "")).startswith(
                f"arXiv:{citation['arxiv']}v"
            )
            or not snapshot.get("retrieved_utc")
            or len(sha256) != 64
            or any(character not in "0123456789abcdef" for character in sha256)
        ):
            raise ValueError(f"study {study['study_id']} has an invalid source snapshot")
        evidence = study.get("evidence")
        if not isinstance(evidence, list) or len(evidence) < 3:
            raise ValueError(f"study {study['study_id']} needs at least three evidence anchors")
        if not all(
            item.get("location")
            and item.get("claim")
            and str(item.get("url", "")).startswith("https://")
            for item in evidence
        ):
            raise ValueError(f"study {study['study_id']} has an invalid evidence anchor")
        if study["kind"] == "simulation":
            scaling = study.get("mass_radius_scaling")
            if not isinstance(scaling, dict) or set(scaling) != {"zh", "en"}:
                raise ValueError(
                    f"simulation study {study['study_id']} requires mass/radius scaling"
                )

    ledger_map = ledger.set_index("prior_id")
    associated: dict[str, set[str]] = {study_id: set() for study_id in study_ids}
    for point in points:
        prior_id = point["prior_id"]
        study_id = point.get("study_id")
        if study_id not in study_map:
            raise ValueError(f"point {prior_id} references unknown study {study_id}")
        associated[study_id].add(prior_id)
        row = ledger_map.loc[prior_id]
        if (
            point.get("doi") != row["doi"]
            or point["doi"] != study_map[study_id]["citation"]["doi"]
        ):
            raise ValueError(f"DOI mismatch for point {prior_id}")
        for field in ("figure_central", "figure_low", "figure_high"):
            if not math.isclose(
                float(point[field]), float(row[field]), rel_tol=0.0, abs_tol=1.0e-12
            ):
                raise ValueError(f"ledger mismatch: numeric drift for {prior_id} field {field}")
        expected_plotted = (
            float(row["figure_all_field_inverse_variance"])
            if pd.notna(row["figure_all_field_inverse_variance"])
            else float(row["figure_central"])
        )
        actual_plotted = float(point.get("plotted_central", point["figure_central"]))
        if not math.isclose(
            actual_plotted, expected_plotted, rel_tol=0.0, abs_tol=1.0e-12
        ):
            raise ValueError(f"plotted-marker drift for {prior_id}")
        for field in ("native_input", "transfer_summary", "interpretation"):
            value = point.get(field)
            if not isinstance(value, dict) or set(value) != {"zh", "en"}:
                raise ValueError(f"point {prior_id} field {field} is not bilingual")
        assumptions = point.get("assumptions")
        if not isinstance(assumptions, list) or not assumptions:
            raise ValueError(f"point {prior_id} requires explicit assumptions")
        if not all(isinstance(item, dict) and set(item) == {"zh", "en"} for item in assumptions):
            raise ValueError(f"point {prior_id} assumptions are not bilingual")
        steps = point.get("transfer_steps")
        if not isinstance(steps, list) or len(steps) < 3:
            raise ValueError(f"point {prior_id} requires at least three transfer steps")
        if not all(
            isinstance(item, dict)
            and {"zh", "en"} <= set(item)
            and isinstance(item["zh"], str)
            and isinstance(item["en"], str)
            for item in steps
        ):
            raise ValueError(f"point {prior_id} transfer steps are not bilingual")

    for study_id, expected in associated.items():
        declared = set(study_map[study_id].get("point_ids", []))
        if declared != expected:
            raise ValueError(f"study/point linkage mismatch for {study_id}")

    serialized = json.dumps(registry, ensure_ascii=False).lower()
    if "source audit pending" in serialized or "待核" in serialized:
        raise ValueError("reference registry contains unresolved audit placeholders")


def load_reference_registry(
    registry_path: Path | None = None, ledger_path: Path | None = None
) -> dict[str, Any]:
    registry_path = registry_path or REFERENCE_REGISTRY
    registry = json.loads(registry_path.read_text(encoding="utf-8"))
    validate_reference_registry(registry, ledger_path)
    return registry


def bilingual_block(tag: str, value: dict[str, str], *, class_name: str = "") -> str:
    class_attribute = f' class="{html.escape(class_name, quote=True)}"' if class_name else ""
    return (
        f"<{tag}{class_attribute} data-zh>{html.escape(value['zh'])}</{tag}>"
        f"<{tag}{class_attribute} data-en>{html.escape(value['en'])}</{tag}>"
    )


def ordered_bilingual_steps(steps: list[dict[str, str]]) -> str:
    items = []
    for step in steps:
        formula = ""
        if step.get("formula"):
            formula = f'<code class="formula">{html.escape(step["formula"])}</code>'
        items.append(
            "<li>"
            f'<span data-zh>{html.escape(step["zh"])}</span>'
            f'<span data-en>{html.escape(step["en"])}</span>{formula}</li>'
        )
    return f'<ol class="transfer-steps">{"".join(items)}</ol>'


def plotted_central(point: dict[str, Any]) -> float:
    """Return the marker actually drawn in Figure 3."""
    return float(point.get("plotted_central", point["figure_central"]))


def render_figure3_reference_index(registry: dict[str, Any]) -> str:
    points_by_study: dict[str, list[dict[str, Any]]] = {}
    for point in registry["points"]:
        points_by_study.setdefault(point["study_id"], []).append(point)
    cards = []
    for study in registry["studies"]:
        point_links = "".join(
            '<li><a class="point-link" '
            f'href="points/{html.escape(point["slug"], quote=True)}/index.html">'
            f'{html.escape(point["figure_label"])} '
            f'<span>{plotted_central(point):.6g}</span></a></li>'
            for point in points_by_study[study["study_id"]]
        )
        cards.append(
            '<article class="study-card">'
            f'<p class="evidence-class">{html.escape(study["evidence_class"])}</p>'
            f'<h4><a href="studies/{html.escape(study["slug"], quote=True)}/index.html">'
            f'{html.escape(study["citation"]["authors"])} ({study["citation"]["year"]})</a></h4>'
            + bilingual_block("p", study["overview"], class_name="study-card-summary")
            + f'<ul class="point-links">{point_links}</ul>'
            + "</article>"
        )
    return (
        '<section class="reference-library" id="figure3-reference-library">'
        '<p class="section-kicker">Figure 3 · source-to-point library</p>'
        '<h3 data-zh>逐篇论文、逐个数据点的推理档案</h3>'
        '<h3 data-en>Paper-by-paper and point-by-point reasoning files</h3>'
        '<p class="lead" data-zh>7个study pages解释原始观测或simulation；12个point pages逐步记录原文量、common-band或M31 transfer、额外假设与Figure 3结果。每个数字都反向绑定frozen ledger。</p>'
        '<p class="lead" data-en>Seven study pages explain the native observations or simulations; twelve point pages trace the published quantity, common-band or M31 transfer, added assumptions, and final Figure 3 result. Every number is bound back to the frozen ledger.</p>'
        f'<div class="study-grid">{"".join(cards)}</div></section>'
    )


def reference_page_shell(
    template: str,
    *,
    page_kind: str,
    title_zh: str,
    title_en: str,
    body: str,
    canonical_path: str,
) -> str:
    replacements = {
        "{{PAGE_KIND}}": html.escape(page_kind, quote=True),
        "{{PAGE_TITLE_ZH_ATTR}}": html.escape(title_zh, quote=True),
        "{{PAGE_TITLE_EN_ATTR}}": html.escape(title_en, quote=True),
        "{{PAGE_TITLE_ZH}}": html.escape(title_zh),
        "{{PAGE_TITLE_EN}}": html.escape(title_en),
        "{{CANONICAL_URL}}": html.escape(CANONICAL_URL + canonical_path, quote=True),
        "{{PAGE_BODY}}": body,
    }
    page = template
    for token, value in replacements.items():
        page = page.replace(token, value)
    if "{{" in page or "}}" in page:
        raise ValueError(f"unresolved reference-page token for {canonical_path}")
    return page


def render_study_page(
    study: dict[str, Any], points: list[dict[str, Any]], template: str
) -> str:
    citation = study["citation"]
    snapshot = citation["source_snapshot"]
    snapshot_block = (
        '<p class="source-snapshot"><strong>Source snapshot:</strong> '
        f'{html.escape(snapshot["version"])} · {html.escape(snapshot["retrieved_utc"])}<br>'
        f'<code>SHA-256 {html.escape(snapshot["sha256"])}</code></p>'
    )
    primary_links = "".join(
        f'<li><a href="{html.escape(url, quote=True)}">{html.escape(label)}</a></li>'
        for label, url in citation["primary_urls"].items()
    )
    point_links = "".join(
        '<article class="point-summary">'
        f'<h3><a href="../../points/{html.escape(point["slug"], quote=True)}/index.html">'
        f'{html.escape(point["figure_label"])}</a></h3>'
        f'<p class="metric-inline">{plotted_central(point):.6f} '
        f'[{float(point["figure_low"]):.6f}, {float(point["figure_high"]):.6f}]</p>'
        + bilingual_block("p", point["interpretation"])
        + "</article>"
        for point in points
    )
    evidence_rows = "".join(
        "<tr>"
        f'<td>{html.escape(item["location"])}</td>'
        f'<td>{html.escape(item["claim"])}</td>'
        f'<td><a href="{html.escape(item["url"], quote=True)}">primary source</a></td>'
        "</tr>"
        for item in study["evidence"]
    )
    sections = [
        ("instrument", "仪器与数据", "Instrument and data", study["instrument"]),
        ("sightlines", "观测视线与样本域", "Sightlines and sample domain", study["sightlines"]),
        ("measurement", "原文测量量", "Native measurement", study["measurement"]),
        ("model", "原文模型", "Published model", study["model"]),
        ("published-results", "原文结果", "Published results", study["published_results"]),
        ("transfer", "如何进入M31 Figure 3", "Transfer into M31 Figure 3", study["transfer_to_m31"]),
        ("limitations", "假设与解释边界", "Assumptions and boundaries", study["limitations"]),
    ]
    if study["kind"] == "simulation":
        sections.insert(
            4,
            (
                "mass-radius-scaling",
                "质量scaling与radial profile",
                "Mass scaling and radial profile",
                study["mass_radius_scaling"],
            ),
        )
    section_html = "".join(
        f'<section class="reference-section" data-section="{section_id}">'
        f'<h2 data-zh>{title_zh}</h2><h2 data-en>{title_en}</h2>'
        + bilingual_block("p", value, class_name="lead")
        + "</section>"
        for section_id, title_zh, title_en, value in sections
    )
    title_zh = f"{citation['authors']}（{citation['year']}）论文档案"
    title_en = f"{citation['authors']} ({citation['year']}) study file"
    body = (
        '<header class="reference-hero"><p class="section-kicker">Figure 3 · study file</p>'
        f'<h1 data-zh>{html.escape(title_zh)}</h1><h1 data-en>{html.escape(title_en)}</h1>'
        f'<p class="citation-title">{html.escape(citation["title"])}</p>'
        + bilingual_block("p", study["overview"], class_name="hero-deck")
        + f'<p><a class="button primary" href="https://doi.org/{html.escape(citation["doi"], quote=True)}">DOI {html.escape(citation["doi"])}</a></p>'
        + snapshot_block
        + "</header>"
        + section_html
        + '<section class="reference-section" data-section="figure3-points"><h2 data-zh>本论文对应的Figure 3数据点</h2><h2 data-en>Figure 3 points from this study</h2>'
        + f'<div class="point-summary-grid">{point_links}</div></section>'
        + '<section class="reference-section" data-section="evidence"><h2 data-zh>Primary-source证据地图</h2><h2 data-en>Primary-source evidence map</h2>'
        + '<div class="table-wrap"><table class="data evidence-table"><thead><tr><th>Location</th><th>Claim</th><th>Link</th></tr></thead>'
        + f'<tbody>{evidence_rows}</tbody></table></div><ul class="primary-links">{primary_links}</ul></section>'
    )
    return reference_page_shell(
        template,
        page_kind="study",
        title_zh=title_zh,
        title_en=title_en,
        body=body,
        canonical_path=f"studies/{study['slug']}/",
    )


def render_point_page(
    point: dict[str, Any], study: dict[str, Any], template: str
) -> str:
    audit_note = ""
    if not math.isclose(
        plotted_central(point),
        float(point["figure_central"]),
        rel_tol=0.0,
        abs_tol=1.0e-12,
    ):
        audit_note = (
            '<p class="metric-note"><span data-zh>实际图中marker采用all-field estimator；'
            f'ledger side-balanced audit summary为 {float(point["figure_central"]):.9g}。</span>'
            '<span data-en>The plotted marker uses the all-field estimator; '
            f'the ledger side-balanced audit summary is {float(point["figure_central"]):.9g}.</span></p>'
        )
    assumptions = "".join(
        f'<li><span data-zh>{html.escape(item["zh"])}</span>'
        f'<span data-en>{html.escape(item["en"])}</span></li>'
        for item in point["assumptions"]
    )
    title_zh = f"{point['figure_label']}：数据点推理"
    title_en = f"{point['figure_label']}: point reasoning"
    body = (
        '<header class="reference-hero"><p class="section-kicker">Figure 3 · point file</p>'
        f'<h1 data-zh>{html.escape(title_zh)}</h1><h1 data-en>{html.escape(title_en)}</h1>'
        f'<p class="prior-id">{html.escape(point["prior_id"])}</p>'
        f'<p><a href="../../studies/{html.escape(study["slug"], quote=True)}/index.html">'
        f'{html.escape(study["citation"]["authors"])} ({study["citation"]["year"]}) study file</a></p></header>'
        '<section class="reference-section" data-section="figure3-result"><h2 data-zh>Figure 3最终值</h2><h2 data-en>Final Figure 3 value</h2>'
        '<div class="result-metric">'
        f'<strong>{plotted_central(point):.9g}</strong>'
        f'<span>{float(point["figure_low"]):.9g} – {float(point["figure_high"]):.9g}</span>'
        '<small>10<sup>−15</sup> erg cm<sup>−2</sup> s<sup>−1</sup> arcmin<sup>−2</sup></small></div>'
        + audit_note
        + bilingual_block("p", point["interpretation"], class_name="lead")
        + '</section><section class="reference-section" data-section="native-input"><h2 data-zh>原文输入量</h2><h2 data-en>Native published input</h2>'
        + bilingual_block("p", point["native_input"], class_name="lead")
        + '</section><section class="reference-section" data-section="conversion-chain"><h2 data-zh>转换链</h2><h2 data-en>Conversion chain</h2>'
        + bilingual_block("p", point["transfer_summary"], class_name="lead")
        + ordered_bilingual_steps(point["transfer_steps"])
        + '</section><section class="reference-section" data-section="assumptions"><h2 data-zh>新增假设与边界</h2><h2 data-en>Added assumptions and boundaries</h2>'
        + f'<ul class="assumption-list">{assumptions}</ul></section>'
        + '<section class="reference-section" data-section="provenance"><h2>Provenance</h2>'
        f'<p>DOI: <a href="https://doi.org/{html.escape(point["doi"], quote=True)}">{html.escape(point["doi"])}</a></p>'
        '<p><a href="../../assets/data/figure3_reference_registry.json">figure3_reference_registry.json</a> · '
        '<a href="../../assets/source/m31_cgmsum_conditional_prior_ledger.csv">frozen Figure 3 ledger</a></p></section>'
    )
    return reference_page_shell(
        template,
        page_kind="point",
        title_zh=title_zh,
        title_en=title_en,
        body=body,
        canonical_path=f"points/{point['slug']}/",
    )
