"""Render the bilingual stacking-profile comparison from literal JSON content."""

from __future__ import annotations

import html
import json
import re
from pathlib import Path
from typing import Any
from urllib.parse import urlparse


LANGUAGE_KEYS = {"zh", "en"}
LANGUAGE_CONTENT_KEYS = {
    "title",
    "description",
    "eyebrow",
    "heading",
    "intro",
    "sections",
    "downloads",
}
SECTION_REQUIRED_KEYS = {"id", "kicker", "heading", "paragraphs"}
SECTION_OPTIONAL_KEYS = {"figures", "links", "table"}
FIGURE_KEYS = {"stem", "caption"}
LINK_KEYS = {"label", "url"}
TABLE_KEYS = {"caption", "columns", "rows"}
DOWNLOAD_KEYS = {"path", "label"}
EMAIL_PATTERN = re.compile(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.I)
SECTION_ID_PATTERN = re.compile(r"^[a-z][a-z0-9-]{0,63}$")
FIGURE_STEM_PATTERN = re.compile(r"^stack_comparison_[a-z0-9_]+$")
DOWNLOAD_PATH_PATTERN = re.compile(r"^assets/data/stack_comparison_[a-z0-9_-]+\.(?:csv|json)$")


def _require_string(value: object, label: str, *, allow_empty: bool = False) -> str:
    if not isinstance(value, str) or (not allow_empty and not value.strip()):
        raise ValueError(f"stack comparison {label} must be a non-empty string")
    if EMAIL_PATTERN.search(value):
        raise ValueError(f"email address is not allowed in stack comparison {label}")
    return value


def _require_string_list(
    value: object, label: str, *, allow_empty: bool = False
) -> list[str]:
    if not isinstance(value, list):
        raise ValueError(f"stack comparison {label} must be a list")
    return [
        _require_string(item, f"{label} item", allow_empty=allow_empty)
        for item in value
    ]


def validate_content(content: object, available_assets: set[str]) -> dict[str, Any]:
    """Validate the exact public content schema and its local asset references."""
    if not isinstance(content, dict) or set(content) != {"schema_version", "languages"}:
        raise ValueError("stack comparison content schema keys mismatch")
    if type(content["schema_version"]) is not int or content["schema_version"] != 1:
        raise ValueError("stack comparison content schema_version mismatch")
    languages = content["languages"]
    if not isinstance(languages, dict) or set(languages) != LANGUAGE_KEYS:
        raise ValueError("stack comparison languages must contain zh and en")

    validated: dict[str, Any] = {"schema_version": 1, "languages": {}}
    seen_ids: dict[str, set[str]] = {}
    language_structure: dict[
        str, list[tuple[str, tuple[str, ...], tuple[str, ...]]]
    ] = {}
    language_download_paths: dict[str, list[str]] = {}
    for language in ("zh", "en"):
        localized = languages[language]
        if not isinstance(localized, dict) or set(localized) != LANGUAGE_CONTENT_KEYS:
            raise ValueError(
                f"stack comparison {language} content keys mismatch: "
                f"expected {sorted(LANGUAGE_CONTENT_KEYS)}"
            )
        copy = {
            key: _require_string(localized[key], f"{language}.{key}")
            for key in ("title", "description", "eyebrow", "heading")
        }
        copy["intro"] = _require_string_list(localized["intro"], f"{language}.intro")
        if not copy["intro"]:
            raise ValueError(f"stack comparison {language}.intro must not be empty")

        sections = localized["sections"]
        if not isinstance(sections, list) or not sections:
            raise ValueError(f"stack comparison {language}.sections must not be empty")
        normalized_sections = []
        ids: set[str] = set()
        stems: set[str] = set()
        for index, section in enumerate(sections):
            if not isinstance(section, dict) or not SECTION_REQUIRED_KEYS <= set(section):
                raise ValueError(f"stack comparison {language} section {index} schema mismatch")
            if set(section) - (SECTION_REQUIRED_KEYS | SECTION_OPTIONAL_KEYS):
                raise ValueError(f"stack comparison {language} section {index} has unknown keys")
            section_id = _require_string(section["id"], f"{language} section id")
            if not SECTION_ID_PATTERN.fullmatch(section_id) or section_id in ids:
                raise ValueError(f"invalid or duplicate stack comparison section id: {section_id}")
            ids.add(section_id)
            normalized = {
                "id": section_id,
                "kicker": _require_string(section["kicker"], f"{language} section kicker"),
                "heading": _require_string(section["heading"], f"{language} section heading"),
                "paragraphs": _require_string_list(
                    section["paragraphs"], f"{language} section paragraphs"
                ),
                "figures": [],
                "links": [],
            }
            if "links" in section:
                links = section["links"]
                if not isinstance(links, list):
                    raise ValueError(f"stack comparison {language} section links must be a list")
                for link in links:
                    if not isinstance(link, dict) or set(link) != LINK_KEYS:
                        raise ValueError(f"stack comparison {language} source link schema mismatch")
                    label = _require_string(link["label"], f"{language} source link label")
                    url = _require_string(link["url"], f"{language} source link URL")
                    parsed = urlparse(url)
                    try:
                        port = parsed.port
                    except ValueError as error:
                        raise ValueError("invalid port in stack comparison source URL") from error
                    if (
                        len(url) > 2048
                        or any(character.isspace() for character in url)
                        or "\\" in url
                        or parsed.scheme != "https"
                        or parsed.hostname not in {"arxiv.org", "zenodo.org"}
                        or port not in {None, 443}
                        or parsed.username is not None
                        or parsed.password is not None
                    ):
                        raise ValueError(
                            f"stack comparison source link must use an arXiv or Zenodo HTTPS URL: {url}"
                        )
                    normalized["links"].append({"label": label, "url": url})
            if "figures" in section:
                figures = section["figures"]
                if not isinstance(figures, list):
                    raise ValueError(f"stack comparison {language} section figures must be a list")
                for figure in figures:
                    if not isinstance(figure, dict) or set(figure) != FIGURE_KEYS:
                        raise ValueError(f"stack comparison {language} figure schema mismatch")
                    stem = _require_string(figure["stem"], f"{language} figure stem")
                    if not FIGURE_STEM_PATTERN.fullmatch(stem):
                        raise ValueError(f"invalid stack comparison figure stem: {stem}")
                    if stem in stems:
                        raise ValueError(f"duplicate stack comparison figure stem: {stem}")
                    stems.add(stem)
                    variants = [
                        f"assets/figures/{stem}_{column}.{suffix}"
                        for column in ("1col", "2col")
                        for suffix in ("png", "pdf")
                    ]
                    missing = [path for path in variants if path not in available_assets]
                    if missing:
                        raise FileNotFoundError(
                            f"stack comparison figure assets missing: {missing}"
                        )
                    normalized["figures"].append(
                        {
                            "stem": stem,
                            "caption": _require_string(
                                figure["caption"], f"{language} figure caption"
                            ),
                        }
                    )
            if "table" in section:
                table = section["table"]
                if not isinstance(table, dict) or set(table) != TABLE_KEYS:
                    raise ValueError(f"stack comparison {language} table schema mismatch")
                columns = _require_string_list(table["columns"], f"{language} table columns")
                rows = table["rows"]
                if not columns or len(columns) > 20 or not isinstance(rows, list) or len(rows) > 500:
                    raise ValueError(f"stack comparison {language} table dimensions are invalid")
                normalized_rows = []
                for row in rows:
                    cells = _require_string_list(
                        row, f"{language} table row", allow_empty=True
                    )
                    if len(cells) != len(columns):
                        raise ValueError(f"stack comparison {language} table row width mismatch")
                    normalized_rows.append(cells)
                normalized["table"] = {
                    "caption": _require_string(table["caption"], f"{language} table caption"),
                    "columns": columns,
                    "rows": normalized_rows,
                }
            normalized_sections.append(normalized)
        seen_ids[language] = ids
        language_structure[language] = [
            (
                section["id"],
                tuple(figure["stem"] for figure in section["figures"]),
                tuple(link["url"] for link in section["links"]),
            )
            for section in normalized_sections
        ]
        copy["sections"] = normalized_sections

        downloads = localized["downloads"]
        if not isinstance(downloads, list):
            raise ValueError(f"stack comparison {language}.downloads must be a list")
        normalized_downloads = []
        language_paths: set[str] = set()
        language_download_paths[language] = []
        for download in downloads:
            if not isinstance(download, dict) or set(download) != DOWNLOAD_KEYS:
                raise ValueError(f"stack comparison {language} download schema mismatch")
            path = _require_string(download["path"], f"{language} download path")
            if not DOWNLOAD_PATH_PATTERN.fullmatch(path):
                raise ValueError(f"invalid stack comparison download path: {path}")
            if path not in available_assets:
                raise FileNotFoundError(f"stack comparison download is missing: {path}")
            if path in language_paths:
                raise ValueError(f"duplicate stack comparison download: {path}")
            language_paths.add(path)
            language_download_paths[language].append(path)
            normalized_downloads.append(
                {"path": path, "label": _require_string(download["label"], f"{language} download label")}
            )
        copy["downloads"] = normalized_downloads
        validated["languages"][language] = copy

    if seen_ids["zh"] != seen_ids["en"]:
        raise ValueError("stack comparison Chinese and English section IDs differ")
    # Figure inventories are bilingual in structure even though captions differ.
    if language_structure["zh"] != language_structure["en"]:
        raise ValueError(
            "stack comparison Chinese and English section/figure/source-link structure differs"
        )
    if language_download_paths["zh"] != language_download_paths["en"]:
        raise ValueError("stack comparison Chinese and English download inventories differ")
    return validated


def load_content(path: Path, available_assets: set[str]) -> dict[str, Any]:
    """Load JSON with duplicate-key rejection, then validate its public schema."""
    def reject_duplicates(pairs: list[tuple[str, object]]) -> dict[str, object]:
        result: dict[str, object] = {}
        for key, value in pairs:
            if key in result:
                raise ValueError(f"duplicate stack comparison JSON key: {key}")
            result[key] = value
        return result

    content = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=reject_duplicates)
    return validate_content(content, available_assets)


def _figure_html(figure: dict[str, str]) -> str:
    stem = figure["stem"]
    caption = html.escape(figure["caption"])
    image = f"/assets/figures/{stem}_2col.png"
    links = []
    for column in ("1col", "2col"):
        for suffix in ("png", "pdf"):
            path = f"/assets/figures/{stem}_{column}.{suffix}"
            links.append(
                f'<a class="figure-download" href="{path}">{column} {suffix.upper()}</a>'
            )
    return (
        '<figure class="presentation-figure">'
        f'<a href="/assets/figures/{stem}_2col.pdf"><img src="{image}" '
        f'alt="{caption}" loading="lazy"></a>'
        f'<figcaption><p>{caption}</p><div class="fig-actions">{"".join(links)}</div></figcaption>'
        "</figure>"
    )


def _table_html(table: dict[str, Any]) -> str:
    headers = "".join(f"<th scope=\"col\">{html.escape(label)}</th>" for label in table["columns"])
    rows = "".join(
        "<tr>" + "".join(f"<td>{html.escape(cell)}</td>" for cell in row) + "</tr>"
        for row in table["rows"]
    )
    return (
        '<div class="table-wrap"><table class="data">'
        f'<caption>{html.escape(table["caption"])}</caption>'
        f"<thead><tr>{headers}</tr></thead><tbody>{rows}</tbody></table></div>"
    )


def render_page(
    language: str,
    content: dict[str, Any],
    template: str,
    asset_sizes: dict[str, int],
) -> str:
    """Render one language route using only validated literal content."""
    if language not in LANGUAGE_KEYS:
        raise ValueError(f"unsupported stack comparison language: {language}")
    localized = content["languages"][language]
    sections = []
    navigation = []
    for section in localized["sections"]:
        heading = html.escape(section["heading"])
        kicker = html.escape(section["kicker"])
        navigation.append(
            f'<a href="#{section["id"]}" aria-label="{heading}">{kicker}</a>'
        )
        paragraphs = "".join(f"<p>{html.escape(item)}</p>" for item in section["paragraphs"])
        source_link_nodes = [
            f'<a href="{html.escape(link["url"], quote=True)}">'
            f'{html.escape(link["label"])}</a>'
            for link in section["links"]
        ]
        source_links = (
            f'<p class="source-links">{" · ".join(source_link_nodes)}</p>'
            if source_link_nodes
            else ""
        )
        figures = "".join(_figure_html(figure) for figure in section["figures"])
        table = _table_html(section["table"]) if "table" in section else ""
        sections.append(
            f'<section class="section" id="{section["id"]}">'
            f'<p class="section-kicker">{html.escape(section["kicker"])}</p>'
            f"<h2>{heading}</h2>{paragraphs}{source_links}{figures}{table}</section>"
        )

    downloads = []
    for item in localized["downloads"]:
        size = asset_sizes[item["path"]]
        downloads.append(
            f'<a class="download" href="/{item["path"]}">'
            f'<span>{html.escape(item["label"])}</span><span>{size:,} B</span></a>'
        )
    if downloads:
        download_section = (
            f'<section class="section" id="downloads"><p class="section-kicker">'
            f'{"数据与文件" if language == "zh" else "Data and files"}</p>'
            f'<h2>{"下载数据与图件" if language == "zh" else "Download data and figures"}</h2>'
            f'<div class="downloads">{"".join(downloads)}</div></section>'
        )
    else:
        download_section = ""

    values = {
        "LANG": "zh-CN" if language == "zh" else "en",
        "TITLE": html.escape(localized["title"]),
        "DESCRIPTION": html.escape(localized["description"], quote=True),
        "EYEBROW": html.escape(localized["eyebrow"]),
        "HEADING": html.escape(localized["heading"]),
        "INTRO": "".join(f"<p class=\"hero-deck\">{html.escape(item)}</p>" for item in localized["intro"]),
        "SECTIONS": "".join(sections),
        "DOWNLOAD_SECTION": download_section,
        "NAVIGATION": "".join(navigation),
        "EXPLORER_LINK": (
            '<a href="../index.html">主页面</a>'
            if language == "zh"
            else '<a href="../index-en.html">Explorer home</a>'
        ),
        "PAPER_LINK": (
            '<a href="https://m31cgm-cgmsum-apj-draft.pages.dev/">论文 draft</a>'
            if language == "zh"
            else '<a href="https://m31cgm-cgmsum-apj-draft.pages.dev/">Paper draft</a>'
        ),
        "HOME_URL": "../index.html" if language == "zh" else "../index-en.html",
        "ZH_PRESSED": "true" if language == "zh" else "false",
        "EN_PRESSED": "true" if language == "en" else "false",
        "SKIP_LABEL": "跳到正文" if language == "zh" else "Skip to content",
        "HOME_LABEL": "返回主页面" if language == "zh" else "Back to explorer home",
        "FOOTER_TEXT": (
            "Stacking profile comparison · bilingual draft page"
            if language == "en"
            else "Stacking profile comparison · 双语 draft 页面"
        ),
    }
    rendered = template
    for key, value in values.items():
        rendered = rendered.replace("{{" + key + "}}", value)
    # The Cloudflare author-name gate <script> legitimately contains "}}"
    # (nested JS closures); drop it before the token check so a gated template
    # is not misreported as having unresolved tokens.
    gate_free = re.sub(r"<script>\s*\(function\(\)\{try\{var HS=.*?</script>", "", rendered, flags=re.S)
    if "{{" in gate_free or "}}" in gate_free:
        raise ValueError(f"unresolved stack comparison template token for {language}")
    return rendered
