"""Build a Hugging Face recipe Space from explicit tracked documentation.

The standard-library build is offline. Publication imports huggingface_hub only
when explicitly requested, checks the personal owner and requested visibility, and
uploads only the reviewed static build. No training data or model weights enter it.
"""
from __future__ import annotations

import argparse
import hashlib
import html
from html.parser import HTMLParser
import json
import os
from pathlib import Path
import posixpath
import re
import subprocess
import tempfile
from urllib.parse import quote, unquote, urlsplit

from .hub_content import render_evidence
from .hub_theme import CSS, JS

SOURCE_URL = "https://github.com/ChTauchmann/hessian-recipes"
RECIPES = [
    ("Evaluation kit", "docs/evaluation.md", "Implemented", "Frozen specifications, adapters, reports and regression checks."),
    ("Reasoning SFT and GRPO", "docs/training.md", "Implemented", "Pinned TRL recipes and a CPU wiring check."),
    ("Sequence distillation", "docs/training.md#distillation-without-a-hosted-teacher", "Implemented", "Local teacher generation, answer verification and filtered SFT data."),
    ("German Qwen SFT", "docs/use-cases/german-reasoning.md", "A100 pilot", "Source-aligned German math adaptation and matched MGSM evaluation."),
    ("Agentic RL", "docs/use-cases/german-agent-rl.md", "A100 pilot", "Native multi-turn tool training, agent evaluation and retention checks."),
    ("RL agent gyms", "docs/agent-gyms.md", "Native integrations", "NeMo Gym with GPU-tested TRL; OpenEnv and Verifiers CPU qualification."),
    ("OpenEnv vs NeMo on German Qwen", "docs/use-cases/german-openenv-comparison.md", "Matched protocol", "Native OpenEnv, shared DEV and MGSM tests, sequential GPU scheduling; quality results pending."),
    ("Existing agent harness", "docs/agent-harness.md", "Adoption pilot", "Trace contract and optional Lightning/verl launcher; Lightning GPU path unqualified."),
    ("Benchmark programme", "benchmarks/README.md", "Designs and fixtures", "German RAG with cited sources and business-agent workflows; expert tasks and benchmark validation remain open."),
    ("Robotics, Bio/Med, materials", "docs/domains.md", "Sketches", "Domain-native starting points and proposed evaluation contracts."),
    ("Releases and Hub artifacts", "docs/releases.md", "Candidate packaging", "Local checksummed bundles and model/dataset card templates."),
]
NAVIGATION = [("Recipes", "index.html"), ("Documentation", "docs/README.md"),
              ("Programme status", "docs/programme-status.md"),
              ("Benchmark designs", "docs/benchmark-designs.md"),
              ("Framework comparison", "docs/environment-frameworks.md"),
              ("Run from Hugging Face", "examples/huggingface/RUN.md"),
              ("Source snapshot", "source-index.html"),
              ("Evidence", "docs/validation.md"), ("Architecture", "docs/architecture.md"),
              ("Related work", "docs/related-work.md"), ("Operations", "docs/operations.md"),
              ("Roadmap", "docs/roadmap.md"), ("HF publishing patterns", "examples/huggingface/README.md")]

SOURCE_TREES = {"src", "requirements", "recipes", "examples", "templates", "benchmarks",
                "cluster", "tests", "docs", "scripts", "containers"}
SOURCE_FILES = {"README.md", "pyproject.toml", "MANIFEST.in", ".gitignore", ".dockerignore",
                "LICENSE", "LICENSE.md", "LICENSE.txt"}
SOURCE_SUFFIXES = {".py", ".md", ".json", ".jsonl", ".yaml", ".yml", ".txt", ".toml", ".sh", ".sbatch"}
BRAND_DIRECTORY = "examples/huggingface/brand/"
# Binary/vector assets are limited to these reviewed institutional brand files.
# Other image/font files in source trees do not enter the published snapshot.
BRAND_FILES = {"hessian-ai-logo.svg", "favicon-32x32.png", "safari-pinned-tab.svg",
               "Poppins-Light.woff2", "Poppins-Regular.woff2", "Poppins-SemiBold.woff2",
               "Poppins-Bold.woff2", "OFL-Poppins.txt", "provenance.json",
               "jaif-logo.png", "jaif-provenance.json"}
BRAND_INPUTS = {BRAND_DIRECTORY + name for name in BRAND_FILES}
# These exact first-review files are also inputs before the first source commit.
REVIEW_INPUTS = {"src/hessian_recipes/hf_recipe_hub.py", "tests/test_hf_recipe_hub.py",
                 "src/hessian_recipes/hub_theme.py", "src/hessian_recipes/hub_content.py",
                 "examples/huggingface/README.md", "examples/huggingface/RUN.md"}
COLLECTION_ITEMS = [
    ("ctauchmann/hessian-recipes", "space", "Private recipe, source and aggregate-evidence hub. Trained artifacts and raw runs remain local; no model or dataset copies are hosted here."),
    ("Qwen/Qwen3-1.7B", "model", "Upstream base model reference. German SFT and agent-RL recipes document local adaptations; the pilots do not establish broad reasoning improvement."),
    ("Mathoctopus/GSM8KInstruct_Parallel", "dataset", "Upstream multilingual reasoning data reference. The German SFT recipe uses canonical-source-aligned, filtered TRAIN examples and source-disjoint DEV; consult the recipe for attribution and filtering."),
    ("juletxara/mgsm", "dataset", "Upstream German mathematical retention evaluation reference; MGSM test is excluded from training. Earlier MGSM64 inspection means the later MGSM250 assessment is not wholly unseen."),
]



def _sha(raw: bytes) -> str:
    return hashlib.sha256(raw).hexdigest()


def _private_space(info) -> bool:
    # Protected Spaces may hide their source while exposing the running App.
    return (info.private is True and getattr(info, "visibility", "private") == "private"
            and getattr(info, "protected", False) is not True)


def _repository_hidden(info, visibility: str = "private") -> bool:
    """Require hidden Hub source; live probes additionally verify App access."""
    if visibility == "private":
        return _private_space(info)
    if visibility != "protected":
        raise ValueError("Only private or protected website visibility is supported")
    return (info.private is True
            and getattr(info, "protected", None) in {None, True}
            and getattr(info, "visibility", None) in {None, "protected"})


def _collection_owner(collection) -> str | None:
    owner = collection.owner
    return owner if isinstance(owner, str) else owner.get("name") if isinstance(owner, dict) else None


def _page(path: str) -> str:
    return "pages/" + path.removesuffix(".md").replace("/", "--") + ".html"


def _slug(value: str) -> str:
    value = re.sub(r"[`*_]", "", value).lower()
    return re.sub(r"[^\w\- ]", "", value).strip().replace(" ", "-")


def _href(target: str, current: str, available: set[str], commit: str) -> str | None:
    target = html.unescape(target)
    parsed = urlsplit(target)
    if parsed.scheme:
        return target if parsed.scheme in {"https", "http", "mailto"} else None
    if target.startswith("//") or target.startswith("/"):
        return None
    if target.startswith("#"):
        return _page(current) + target
    path, _, anchor = target.partition("#")
    resolved = posixpath.normpath(posixpath.join(posixpath.dirname(current), path))
    if resolved.startswith("../"):
        return None
    if resolved in available:
        value = _page(resolved) if resolved.endswith(".md") else "sources/" + resolved
        return value + ("#" + anchor if anchor else "")
    prefix = resolved.rstrip("/") + "/"
    if any(name.startswith(prefix) for name in available):
        return "source-index.html#" + _slug(resolved.rstrip("/"))
    return SOURCE_URL + "/blob/" + commit + "/" + quote(resolved) + ("#" + anchor if anchor else "")


def _inline(value: str, current: str, available: set[str], commit: str) -> str:
    pattern = r"(`[^`]+`|\[[^\]]+\]\([^\)]+\)|\*\*[^*]+\*\*)"
    result = []
    for piece in re.split(pattern, value):
        if piece.startswith("`") and piece.endswith("`"):
            result.append("<code>" + html.escape(piece[1:-1]) + "</code>")
        elif piece.startswith("**") and piece.endswith("**"):
            result.append("<strong>" + html.escape(piece[2:-2]) + "</strong>")
        elif match := re.fullmatch(r"\[([^\]]+)\]\(([^\)]+)\)", piece):
            destination = _href(match[2], current, available, commit)
            label = html.escape(match[1])
            result.append(f'<a href="{html.escape(destination, quote=True)}">{label}</a>' if destination else label)
        else:
            result.append(html.escape(piece))
    return "".join(result)


def render_markdown(text: str, current: str, available: set[str], commit: str) -> str:
    """Render the documentation subset: headings, fences, tables, lists and links.

    Raw HTML is escaped. All generated links are rooted at the Space app's root,
    making subpages work equally from the Space iframe and its standalone URL.
    """
    lines, output, index, code_number = text.splitlines(), [], 0, 0
    def inline(value: str) -> str:
        return _inline(value, current, available, commit)
    while index < len(lines):
        line = lines[index]
        if not line.strip():
            index += 1
            continue
        if line.startswith("```"):
            code_number += 1
            language, block = line[3:].strip(), []
            index += 1
            while index < len(lines) and not lines[index].startswith("```"):
                block.append(lines[index])
                index += 1
            output.append('<div class="code-block"><button type="button" class="copy-button" '
                          'data-copy-target="code-' + str(code_number) + '" aria-label="Copy code block">Copy</button>'
                          '<pre tabindex="0"><code id="code-' + str(code_number) + '" class="language-'
                          + html.escape(language, quote=True) + '">'
                          + html.escape("\n".join(block)) + "</code></pre></div>")
        elif heading := re.match(r"^(#{1,6})\s+(.+)$", line):
            level, title = len(heading[1]), heading[2]
            output.append(f'<h{level} id="{html.escape(_slug(title), quote=True)}">{inline(title)}</h{level}>')
        elif (line.lstrip().startswith("|") and index + 1 < len(lines)
              and re.fullmatch(r"[|\s:\-]+", lines[index + 1]) is not None):
            header = line.strip().strip("|").split("|")
            output.append('<table tabindex="0"><thead><tr>' + "".join("<th>" + inline(cell.strip()) + "</th>" for cell in header) + "</tr></thead><tbody>")
            index += 2
            while index < len(lines) and lines[index].lstrip().startswith("|"):
                cells = lines[index].strip().strip("|").split("|")
                output.append("<tr>" + "".join("<td>" + inline(cell.strip()) + "</td>" for cell in cells) + "</tr>")
                index += 1
            output.append("</tbody></table>")
            continue
        elif re.match(r"^\s*(?:[-*]|\d+[.])\s+", line):
            ordered = bool(re.match(r"^\s*\d+[.]", line))
            tag = "ol" if ordered else "ul"
            output.append("<" + tag + ">")
            while index < len(lines) and (item := re.match(r"^\s*(?:[-*]|\d+[.])\s+(.+)$", lines[index])):
                output.append("<li>" + inline(item[1]) + "</li>")
                index += 1
            output.append("</" + tag + ">")
            continue
        elif line.startswith("> "):
            output.append("<blockquote>" + inline(line[2:]) + "</blockquote>")
        else:
            paragraph = [line]
            index += 1
            while index < len(lines) and lines[index].strip() and not re.match(r"^(?:#|```|\||[-*] |\d+[.] )", lines[index]):
                paragraph.append(lines[index])
                index += 1
            output.append("<p>" + inline(" ".join(paragraph)) + "</p>")
            continue
        index += 1
    return "\n".join(output)


def _icon(name: str, class_name: str = "icon") -> str:
    paths = {
        "arrow": '<path d="M5 12h14M12 5l7 7-7 7"/>',
        "train": '<path d="M4 19V5m0 14h16M8 14l4-5 4 2 4-6"/><circle cx="12" cy="9" r="1"/>',
        "evaluate": '<path d="M9 5H5v15h14V5h-4M9 3h6v4H9zM8 13l3 3 5-6"/>',
        "agents": '<rect x="4" y="7" width="16" height="13" rx="3"/><path d="M12 3v4M8 12h.01M16 12h.01M8 16h8M1 12h3m16 0h3"/>',
        "explore": '<path d="M9 3h6M10 3v6L4 19c-.5 1 .5 2 1 2h14c1 0 1.5-1 1-2L14 9V3M8 14h8"/>',
        "lock": '<rect x="5" y="10" width="14" height="11" rx="2"/><path d="M8 10V7a4 4 0 018 0v3m-4 4v3"/>',
        "robotics": '<path d="M4 20h16M7 20v-6l5-5-3-4 3-2 5 7-5 5v5M16 10l3-1 2 3-3 2"/><circle cx="12" cy="9" r="2"/>',
        "bio": '<path d="M7 3c0 8 10 10 10 18M17 3C17 11 7 13 7 21M8 5h8M8 19h8M9 9h6m-6 6h6"/>',
        "materials": '<path d="M12 2l9 5v10l-9 5-9-5V7zM3 7l9 5 9-5M12 12v10M7 5l10 14"/>',
    }
    return ('<svg class="' + html.escape(class_name, quote=True)
            + '" width="24" height="24" viewBox="0 0 24 24" fill="none" '
              'stroke="currentColor" stroke-width="1.5" stroke-linecap="round" '
              'stroke-linejoin="round" aria-hidden="true">' + paths[name] + '</svg>')


def _outline(text: str, current: str) -> str:
    links, in_code, seen = [], False, set()
    for line in text.splitlines():
        if line.startswith("```"):
            in_code = not in_code
        if not in_code and (match := re.match(r"^(#{2,3})\s+(.+)$", line)):
            anchor = _slug(match[2])
            if anchor in seen:
                continue
            seen.add(anchor)
            label = re.sub(r"[`*_]", "", match[2])
            links.append('<a class="toc-level-' + str(len(match[1])) + '" href="'
                         + html.escape(_page(current) + '#' + anchor, quote=True)
                         + '">' + html.escape(label) + '</a>')
    if not links:
        return ""
    return '<aside class="article-toc"><nav aria-label="On this page"><h2>On this page</h2>' + ''.join(links) + '</nav></aside>'


def _document(title: str, body: str, available: set[str], commit: str, *,
              nested: bool = False, landing: bool = False, current: str = "") -> str:
    base = '<base href="../">' if nested else ""
    navigation = []
    for label, target in NAVIGATION:
        destination = target if target.endswith(".html") else _page(target)
        active = ' aria-current="page"' if current == target else ''
        navigation.append(f'<a href="{destination}"{active}>{html.escape(label)}</a>')
    top_links = ('<a href="index.html#recipes">Recipes</a><a href="index.html#workflow">Workflow</a>'
                 '<a href="index.html#evidence">Evidence</a><a href="index.html#benchmarks">Benchmarks</a>'
                 '<a href="pages/docs--README.html">Documentation ' + _icon("arrow") + '</a>')
    mark = ('<img class="brand-logo" src="assets/brand/hessian-ai-logo.svg" alt="" width="308" height="63">'
            if BRAND_DIRECTORY + "hessian-ai-logo.svg" in available else '<span>hessian AI</span>')
    jaif_mark = ('<img class="brand-jaif-logo" src="assets/brand/jaif-logo.png" alt="" width="4489" height="2544">'
                 if BRAND_DIRECTORY + "jaif-logo.png" in available else '<span>JAIF</span>')
    favicon = ('<link rel="icon" type="image/png" sizes="32x32" href="assets/brand/favicon-32x32.png">'
               if BRAND_DIRECTORY + "favicon-32x32.png" in available else '')
    brand = mark + '<span class="brand-ampersand" aria-hidden="true">&amp;</span>' + jaif_mark
    side = '' if landing else '<nav class="side-nav" aria-label="Documentation"><span class="nav-caption">RESEARCH RECIPES</span>' + ''.join(navigation) + '</nav>'
    skip = (_page(current) if nested else 'index.html' if landing else 'source-index.html') + '#main'
    css_asset = 'assets/site.css?v=' + _sha(CSS.encode())[:12]
    js_asset = 'assets/site.js?v=' + _sha(JS.encode())[:12]
    return ('<!doctype html><html lang="en"><head><meta charset="utf-8">'
            '<meta name="viewport" content="width=device-width,initial-scale=1">' + base
            + '<meta http-equiv="Content-Security-Policy" content="default-src \'self\'; style-src \'self\'; script-src \'self\'; font-src \'self\'; img-src \'self\' data: https://us.aws.cdn.hf.co; base-uri \'self\'">'
            '<meta name="description" content="Research recipes, experimental protocols and recorded results for post-training and evaluation.">'
            + "<title>" + html.escape(title) + ' · hessian AI &amp; JAIF</title><link rel="stylesheet" href="' + css_asset + '">'
            + favicon + '</head>'
            '<body class="' + ('home-page' if landing else 'document-page') + '">'
            '<a class="skip-link" href="' + html.escape(skip, quote=True) + '">Skip to content</a>'
            '<header class="site-header"><div class="topbar"><a class="brand" href="index.html" aria-label="hessian AI &amp; JAIF — recipes home">'
            + brand + '</a>'
            '<nav class="top-links" aria-label="Primary">' + top_links + '</nav>'
            '<span class="private-badge">' + _icon('explore') + '<span class="private-label" aria-hidden="true">Research recipes</span><span class="sr-only">Research recipes</span></span>'
            '<button type="button" class="menu-toggle" data-nav-toggle aria-controls="mobile-nav" aria-expanded="false">Menu</button>'
            '</div><nav id="mobile-nav" class="mobile-nav" aria-label="Mobile navigation" hidden>' + top_links + '</nav></header>'
            '<div class="shell' + (' landing' if landing else '') + '">' + side + '<main id="main" class="content" tabindex="-1">' + body
            + '</main></div><footer class="site-footer"><div class="footer-top"><a class="brand" href="index.html" aria-label="hessian AI &amp; JAIF — recipes home">'
            + brand + '</a>'
            '<p>Post-training recipes<br>and evaluation protocols</p><div class="footer-links">'
            '<a href="pages/docs--programme-status.html">Programme status</a>'
            '<a href="pages/docs--related-work.html">Related work</a>'
            '<a href="source-index.html">Browse the source</a>'
            '<a href="hub-manifest.json">Build provenance</a></div></div>'
            '<div class="footer-bottom"><span>hessian AI &amp; JAIF · Research recipes</span><span>Source snapshot '
            + html.escape(commit[:7]) + ' · <a href="' + SOURCE_URL + '">GitHub</a></span></div>'
            '</footer><script src="' + js_asset + '" defer></script></body></html>')


def _landing(snapshots: dict[str, bytes], available: set[str], commit: str) -> str:
    def link(path):
        return html.escape(_href(path, "README.md", available, commit), quote=True)

    hero = ('<section class="hero" aria-labelledby="hero-title"><div class="hero-copy">'
            '<p class="eyebrow">RESEARCH RECIPES · WORK IN PROGRESS</p>'
            '<h1 id="hero-title">Post-training and evaluation</h1>'
            '<p class="hero-description">Reproducible workflows for reasoning adaptation, distillation and agentic reinforcement learning. '
            'Experiments record baseline identities, task splits, training settings and matched evaluation.</p>'
            '<p class="hero-scope">Current case study: German Qwen3-1.7B, with agent task success and MGSM mathematical retention assessed separately.</p>'
            '<div class="hero-actions"><a class="button primary" href="#recipes">Recipes and code ' + _icon('arrow') + '</a>'
            '<a class="button text" href="#evidence">Experimental results ' + _icon('arrow') + '</a></div>'
            '<dl class="hero-stats"><div class="hero-stat"><dt>Model</dt><dd>Qwen3-1.7B</dd></div>'
            '<div class="hero-stat"><dt>Methods</dt><dd>SFT · GRPO</dd></div>'
            '<div class="hero-stat"><dt>Assessment</dt><dd>MGSM · tool use</dd></div></dl></div>'
            '<div class="hero-visual"><div class="diagram-header"><span>EXPERIMENT STRUCTURE</span><span>01—04</span></div>'
            '<div class="workflow-diagram" role="img" aria-label="Freeze a baseline, adapt the model, evaluate held-out tasks, record the evidence.">')
    for number, title, detail, tag in [
        ('01', 'Freeze the baseline', 'Checkpoint · tokenizer · task splits', 'FIX INPUTS'),
        ('02', 'Adapt the model', 'SFT · distillation · agentic RL', 'TRAIN'),
        ('03', 'Evaluate held-out tasks', 'Task success · retention · failure modes', 'COMPARE'),
        ('04', 'Record results and limitations', 'Matched reports · provenance · limits', 'REVIEW'),
    ]:
        hero += ('<div class="diagram-node"><span class="node-num">' + number + '</span><span class="node-copy"><strong>'
                 + title + '</strong><small>' + detail + '</small></span><span class="node-tag">' + tag + '</span></div>')
        if number != '04':
            hero += '<span class="diagram-arrow" aria-hidden="true">↓</span>'
    hero += ('</div><div class="diagram-footer"><span>Versioned inputs and paired assessment</span>'
             '<span class="diagram-label">LOCAL COMPUTE</span></div></div></section>')
    workflow = ('<section id="workflow" class="workflow-section"><div class="section-heading"><div>'
                '<p class="section-kicker">01 / METHODS</p><h2>Experimental protocol</h2></div>'
                '<p>Fixed baselines, development-set selection and separate assessment of task success and retained capabilities.</p></div><div class="workflow-grid">')
    for number, icon, title, description, detail in [
        ('01', 'evaluate', 'Establish a baseline', 'Freeze the model, data and evaluation before you train.', 'Bind the checkpoint, tokenizer, prompts, task splits and decoding settings to a reproducible specification.'),
        ('02', 'train', 'Choose the adaptation', 'Use SFT, distillation or RL for the supervision you have.', 'Reuse pinned upstream trainers and a verifier that reflects success in the target task.'),
        ('03', 'agents', 'Test the whole system', 'Evaluate the tool loop and retained model capabilities.', 'Select on development tasks. Keep final agent assessment and mathematical retention checks separate from training reward.'),
        ('04', 'explore', 'Report the evidence', 'Save the inputs, outcomes and limits behind the claim.', 'Recorded reports include source and artifact identities, paired outcomes and explicit qualification boundaries.'),
    ]:
        workflow += ('<div class="workflow-step"><span class="step-number">' + number + '</span>' + _icon(icon)
                     + '<h3>' + title + '</h3><p>' + description + '</p><details><summary>Protocol details</summary><p>' + detail + '</p></details></div>')
    workflow += '</div></section>'
    categories = ['evaluate', 'train', 'train', 'train', 'agents', 'agents', 'agents', 'agents', 'explore', 'explore', 'evaluate']
    catalogue = ('<section id="recipes" class="recipes"><div class="section-heading"><div><p class="section-kicker">02 / RECIPE COLLECTION</p>'
                 '<h2>Recipes and reference implementations</h2></div><a class="text-link" href="' + link('docs/README.md') + '">All documentation ' + _icon('arrow') + '</a></div>'
                 '<div class="recipe-tools"><div class="search-field"><label class="sr-only" for="filter">Search recipes</label>'
                 '<input id="filter" type="search" placeholder="Search a method, model or task…"></div>'
                 '<div class="filter-controls" role="group" aria-label="Filter recipes by category">')
    for value, label in [('all', 'All recipes'), ('train', 'Train'), ('evaluate', 'Evaluate'), ('agents', 'Agents'), ('explore', 'Explore')]:
        catalogue += '<button type="button" data-category="' + value + '" aria-pressed="' + ('true' if value == 'all' else 'false') + '">' + label + '</button>'
    catalogue += ('</div></div><p id="recipe-count" class="result-count" aria-live="polite">11 recipes &amp; guides</p>'
                  '<div class="recipe-index-header" aria-hidden="true"><span>Method</span><span>Scope</span><span>Status</span><span>Category</span></div><div class="recipe-grid">')
    for number, ((title, target, status, description), category) in enumerate(zip(RECIPES, categories, strict=True), 1):
        catalogue += ('<section class="recipe-card" data-category="' + category + '"><div class="recipe-top"><span class="recipe-number">'
                      + f'{number:02d}' + '</span><span class="badge">' + html.escape(status) + '</span></div>' + _icon(category, 'recipe-icon')
                      + '<h3><a href="' + link(target) + '">' + html.escape(title) + '</a></h3><p>' + html.escape(description)
                      + '</p><div class="recipe-bottom"><span>' + category.upper() + '</span>' + _icon('arrow') + '</div></section>')
    catalogue += '</div><p id="recipe-empty" class="empty-state" hidden>No recipes match. Try another search or category.</p></section>'
    feature = ('<section id="german-qwen" class="feature-story"><div class="feature-copy"><p class="section-kicker">CASE STUDY / GERMAN REASONING</p>'
               '<h2>German Qwen3-1.7B: SFT and agent RL</h2><p>A local Qwen3-1.7B checkpoint is adapted using German SFT and '
               'multi-turn agent RL, then compared with its baseline under matched evaluation settings.</p>'
               '<div class="hero-actions"><a class="button primary" href="' + link('docs/use-cases/german-reasoning.md') + '">Open the use case ' + _icon('arrow') + '</a>'
               '<a class="button secondary" href="' + link('docs/use-cases/german-openenv-comparison.md') + '">OpenEnv × NeMo</a></div>'
               '<p class="feature-footnote">A research pilot. No candidate is qualified for release.</p></div>'
               '<div class="feature-panel"><div class="panel-heading"><span>QWEN3</span><strong>1.7B</strong></div>'
               '<div class="feature-tags"><span>Local checkpoint</span><span>LoRA / BF16</span><span>German</span></div>'
               '<ol class="protocol-list"><li><span>SFT</span><strong>German explanation data</strong></li>'
               '<li><span>AGENT RL</span><strong>Lookup → calculator → answer</strong></li>'
               '<li><span>ASSESSMENT</span><strong>Agent success + MGSM retention</strong></li></ol>'
               '<a class="text-link" href="' + link('docs/use-cases/german-agent-rl-results.md') + '">Read the recorded outcomes ' + _icon('arrow') + '</a></div></section>')
    benchmarks = ('<section id="benchmarks" class="benchmark-section"><div class="section-heading"><div><p class="section-kicker">04 / THE BENCHMARK PROGRAMME</p>'
                  '<h2>Proposed benchmarks</h2></div><p>Two open benchmark designs. Expert task authoring and benchmark validation remain outstanding.</p></div><div class="benchmark-grid">')
    for number, title, description, nodes in [
        ('01', 'German RAG with cited sources', 'Assess retrieval, citation support and abstention using German source material.', ['Retrieve', 'Answer', 'Cite']),
        ('02', 'Business-communication agent workflows', 'Assess final workflow state, permission constraints, approvals and action side effects.', ['Observe', 'Act', 'Verify']),
    ]:
        benchmarks += ('<div class="benchmark-card"><div class="recipe-top"><span class="benchmark-number">' + number
                       + '</span><span class="badge">Proposed benchmark</span></div><h3>' + title + '</h3><p>' + description
                       + '</p><div class="benchmark-diagram" aria-label="' + ' then '.join(nodes) + '">'
                       + '<span class="mini-arrow" aria-hidden="true">→</span>'.join('<span class="mini-node">' + node + '</span>' for node in nodes)
                       + '</div><a class="text-link" href="' + link('docs/benchmark-designs.md') + '">Benchmark design ' + _icon('arrow') + '</a></div>')
    benchmarks += ('</div><p class="section-note">These benchmarks are not released. The repository’s small synthetic development tracks test the evaluation pipeline.</p></section>')
    domains = ('<section id="domains" class="domain-section"><div class="section-heading"><div><p class="section-kicker">05 / DOMAIN DIRECTIONS</p><h2>Domain sketches</h2></div>'
               '<p>Domain-specific starting points, kept as sketches until a team, dataset and metric are defined.</p></div><div class="domain-grid">')
    for icon, title, description in [('robotics', 'Robotics', 'Policies, embodied tasks and verifiable environment outcomes.'),
                                     ('bio', 'Bio / Med', 'Biological models, domain data and task-specific evaluation.'),
                                     ('materials', 'Materials', 'Scientific prediction, simulation and property constraints.')]:
        domains += ('<a class="domain-card" href="' + link('docs/domains.md') + '">' + _icon(icon) + '<span class="badge">Sketch</span><h3>'
                    + title + '</h3><p>' + description + '</p>' + _icon('arrow') + '</a>')
    domains += '</div></section>'
    command = ('git clone https://github.com/ChTauchmann/hessian-recipes.git\ncd hessian-recipes\n'
               'uv venv .venv --python python3.11\nuv pip install --python .venv/bin/python -e .\n'
               '.venv/bin/hessian-recipes demo --output runs/first-demo')
    start = ('<section id="start" class="start-panel"><div class="start-copy"><p class="section-kicker">LOCAL SETUP</p><h2>Run the evaluation fixture</h2>'
             '<p>Run the evaluation demo with authored fixtures. No model, GPU or provider API key required.</p>'
             '<a class="text-link" href="' + link('README.md#first-run-evaluation-without-a-gpu') + '">Read the setup guide ' + _icon('arrow') + '</a>'
             '<p class="start-note">Private GitHub access and UV are required. The demo checks the pipeline; it does not measure a model.</p></div>'
             '<div class="start-terminal"><div class="terminal-header"><span>TERMINAL / PYTHON 3.11+</span>'
             '<button type="button" class="copy-button" data-copy-target="quickstart" aria-label="Copy quickstart commands">Copy</button></div>'
             '<pre tabindex="0"><code id="quickstart">' + html.escape(command) + '</code></pre></div></section>')
    evidence = render_evidence(snapshots, lambda path: _href(path, "README.md", available, commit))
    return hero + workflow + catalogue + feature + evidence + benchmarks + domains + start


class _Links(HTMLParser):
    def __init__(self) -> None:
        super().__init__()
        self.links, self.ids, self.base = [], set(), ""

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        values = dict(attrs)
        if values.get("id"):
            self.ids.add(values["id"])
        if tag == "base":
            self.base = values.get("href", "")
        elif tag in {"a", "link", "script", "img"}:
            link = values.get("href") or values.get("src")
            if link:
                self.links.append(link)


def validate_links(bundle: str | Path) -> dict:
    """Check every local rendered link/asset and HTML anchor, without network IO."""
    root, documents, checked = Path(bundle).resolve(), {}, 0
    for path in root.rglob("*.html"):
        if path.relative_to(root).parts[0] == "sources":
            continue
        parser = _Links()
        parser.feed(path.read_text())
        documents[path.resolve()] = parser
    for path, parser in documents.items():
        for target in parser.links:
            parsed = urlsplit(target)
            if parsed.scheme or target.startswith("//"):
                continue
            basedir = (path.parent / parser.base).resolve() if parser.base else path.parent
            candidate = (basedir / unquote(parsed.path)).resolve() if parsed.path else path
            if not candidate.is_relative_to(root) or not candidate.is_file():
                raise ValueError(f"Broken local link in {path.name}: {target}")
            if parsed.fragment and candidate in documents and unquote(parsed.fragment) not in documents[candidate].ids:
                raise ValueError(f"Broken local anchor in {path.name}: {target}")
            checked += 1
    return {"html_documents": len(documents), "local_links_checked": checked, "broken_local_links": 0}


def build(repo: str | Path, output: str | Path, *, visibility: str = "private") -> dict:
    if visibility not in {"private", "protected"}:
        raise ValueError("Only private or protected website visibility is supported")
    root, destination = Path(repo).resolve(), Path(output).resolve()
    if destination.exists():
        raise ValueError("Use a fresh build directory")
    tracked = set(subprocess.check_output(["git", "ls-files"], cwd=root, text=True).splitlines())
    selected = {name for name in tracked if name in SOURCE_FILES or name in BRAND_INPUTS
                or (Path(name).parts[0] in SOURCE_TREES
                    and (Path(name).suffix in SOURCE_SUFFIXES or Path(name).name == "Dockerfile"))}
    selected |= {name for name in REVIEW_INPUTS if (root / name).is_file()}
    required = {target.split("#")[0] for _, target, _, _ in RECIPES} | {
        "docs/README.md", "pyproject.toml", "examples/huggingface/README.md", "examples/huggingface/RUN.md"}
    if required - selected:
        raise ValueError("Missing recipe pages: " + ", ".join(sorted(required - selected)))
    snapshots = {}
    for name in sorted(selected):
        path = root / name
        if (any((part.startswith(".") and part not in {".gitignore", ".dockerignore"})
                or part.lower() in {"runs", "__pycache__", "credentials.json", "secrets.json", "token.json", "id_rsa", "id_ed25519"}
                for part in Path(name).parts)
                or any((root / Path(*Path(name).parts[:index])).is_symlink()
                       for index in range(1, len(Path(name).parts) + 1))
                or not path.is_file() or not path.resolve().is_relative_to(root)):
            raise ValueError("Only regular allowlisted repository source files may enter the hub")
        raw = path.read_bytes()
        if len(raw) > 2_000_000:
            raise ValueError("Documentation input exceeds the 2 MB bound")
        if (re.search(rb"hf_[A-Za-z0-9]{30,}", raw)
                or re.search(rb"-----BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY-----", raw)):
            raise ValueError("Credential-like literal found in an allowlisted source file")
        snapshots[name] = raw
    if sum(map(len, snapshots.values())) > 25_000_000:
        raise ValueError("Source snapshot exceeds the 25 MB bound")
    commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=root, text=True).strip()
    changed = set(subprocess.check_output(["git", "diff", "HEAD", "--name-only"], cwd=root, text=True).splitlines())
    destination.parent.mkdir(parents=True, exist_ok=True)
    with tempfile.TemporaryDirectory(prefix=".recipe-hub-", dir=destination.parent) as temporary:
        stage = Path(temporary) / "space"
        stage.mkdir()
        (stage / "assets").mkdir()
        (stage / "assets/site.css").write_text(CSS)
        (stage / "assets/site.js").write_text(JS)
        for name in sorted(BRAND_INPUTS & snapshots.keys()):
            asset = stage / "assets/brand" / Path(name).name
            asset.parent.mkdir(exist_ok=True)
            asset.write_bytes(snapshots[name])
        (stage / "pages").mkdir()
        for name, raw in snapshots.items():
            source = stage / "sources" / name
            source.parent.mkdir(parents=True, exist_ok=True)
            source.write_bytes(raw)
            if name.endswith(".md"):
                title = next((line.lstrip("# ") for line in raw.decode().splitlines() if line.startswith("# ")), name)
                article = ('<div class="article-head"><p class="breadcrumbs"><a href="index.html">Home</a>'
                           ' / <a href="pages/docs--README.html">Documentation</a></p>'
                           '<div class="article-meta"><span class="section-kicker">RESEARCH DOCUMENTATION</span>'
                           '<a class="source-link" href="sources/' + html.escape(name, quote=True)
                           + '">View Markdown source ' + _icon('arrow') + '</a></div></div>'
                           '<div class="article-layout"><article class="article-text">'
                           + render_markdown(raw.decode(), name, selected, commit) + '</article>'
                           + _outline(raw.decode(), name) + '</div>')
                (stage / _page(name)).write_text(_document(title, article, selected, commit, nested=True, current=name))
        (stage / "index.html").write_text(_document("Post-training and evaluation", _landing(snapshots, selected, commit),
                                                   selected, commit, landing=True, current="index.html"))
        source_list = "<div class=\"article-text source-browser\"><p class=\"section-kicker\">SOURCE AND PROVENANCE</p><h1>Included source snapshot</h1><p>Source, tests, recipes and documentation for this version. Execution instructions describe local environments; model weights and downloaded data stay on the cluster.</p>"
        for directory in sorted({str(Path(name).parent) for name in snapshots}):
            source_list += '<h2 id="' + _slug(directory) + '">' + html.escape(directory) + '</h2><div class="artifact-list">'
            for name in sorted(name for name in snapshots if str(Path(name).parent) == directory):
                source_list += '<a href="sources/' + html.escape(name, quote=True) + '">' + html.escape(Path(name).name) + '</a>'
            source_list += '</div>'
        (stage / "source-index.html").write_text(_document("Included source", source_list + "</div>", selected, commit, current="source-index.html"))
        (stage / "README.md").write_text("---\ntitle: hessian AI & JAIF\nemoji: 🧪\ncolorFrom: purple\ncolorTo: indigo\nsdk: static\napp_file: index.html\nfullWidth: true\nheader: mini\nshort_description: Research recipes, protocols and recorded evaluation results.\npinned: false\n---\n\n# hessian AI & JAIF\n\nPost-training recipes, evaluation protocols and recorded research evidence. Open the App tab for workflow pages.\n\nBuilt from explicit repository documentation; model weights and raw run data are excluded.\n")
        files = {str(path.relative_to(stage)): _sha(path.read_bytes()) for path in sorted(stage.rglob("*")) if path.is_file()}
        manifest = {"schema_version": 1, "intended_visibility": visibility, "source_repository": SOURCE_URL,
                    "source_commit": commit, "source_sha256": {name: _sha(raw) for name, raw in snapshots.items()},
                    "source_snapshot_note": "Exact bytes are bound by source_sha256; HEAD is the history reference.",
                    "uncommitted_source_paths": sorted((selected & changed) | (selected - tracked)),
                    "files_sha256": files, "recipe_count": len(RECIPES),
                    "page_count": len(list((stage / "pages").glob("*.html"))) + 2,
                    "source_file_count": len(snapshots), "source_bytes": sum(map(len, snapshots.values())),
                    "long_experiment_status": "No new long-run outcomes included in this source snapshot.", "contains_weights_or_raw_runs": False,
                    "included_data": "Only small git-tracked authored development fixtures; no downloaded training/evaluation data."}
        # The manifest is itself a linked JSON artifact, so create it before link validation.
        (stage / "hub-manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n")
        manifest["link_validation"] = validate_links(stage)
        (stage / "hub-manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n")
        os.replace(stage, destination)
    return manifest


def publish(bundle: str | Path, repo_id: str, *, expected_owner: str = "ctauchmann", api=None,
            publisher_token: str | None = None, mirror: bool = False, visibility: str = "private") -> dict:
    """Publish reviewed website bytes without changing repository visibility."""
    if visibility not in {"private", "protected"}:
        raise ValueError("Only private or protected website visibility is supported")
    if repo_id != expected_owner + "/hessian-recipes":
        raise ValueError("Publication is restricted to the approved personal recipe Space")
    if publisher_token is not None and not publisher_token.startswith("hf_jwt_"):
        raise ValueError("Trusted publishing requires a repository-scoped OIDC token")
    if api is None:
        from huggingface_hub import HfApi
        api = HfApi(token=publisher_token)
    if publisher_token is not None and getattr(api, "token", None) != publisher_token:
        raise ValueError("Publisher API must use the exchanged repository token")
    # OIDC repo publishers are synthetic identities. The Hub authenticates their
    # short-lived token against this exact repo; personal tokens retain the
    # existing account-identity check. Neither path changes repository visibility.
    if publisher_token is None and api.whoami().get("name") != expected_owner:
        raise ValueError("Authenticated Hub identity differs from the approved owner")
    folder = Path(bundle).resolve()
    manifest = json.loads((folder / "hub-manifest.json").read_text())
    if manifest.get("intended_visibility") != visibility:
        raise ValueError("Build visibility differs from the explicit publication target")
    actual = {str(path.relative_to(folder)) for path in folder.rglob("*") if path.is_file()}
    if any(path.is_symlink() for path in folder.rglob("*")):
        raise ValueError("The reviewed bundle cannot contain symbolic links")
    if actual != set(manifest["files_sha256"]) | {"hub-manifest.json"}:
        raise ValueError("The reviewed bundle inventory changed")
    for name, digest in manifest["files_sha256"].items():
        if Path(name).is_absolute() or ".." in Path(name).parts:
            raise ValueError("The reviewed bundle contains an unsafe path")
        if _sha((folder / name).read_bytes()) != digest:
            raise ValueError("The reviewed bundle bytes changed")
    if not api.repo_exists(repo_id, repo_type="space"):
        if publisher_token is not None or mirror or visibility == "protected":
            raise ValueError("Automated or protected publication requires the existing hidden Space")
        api.create_repo(repo_id, repo_type="space", space_sdk="static", private=True, exist_ok=False)
    info = api.repo_info(repo_id, repo_type="space")
    if not _repository_hidden(info, visibility) or info.id != repo_id or getattr(info, "sdk", "static") != "static":
        raise ValueError("Refusing to upload to an existing public or differently owned Space")
    options = {}
    if mirror:
        obsolete = set(api.list_repo_files(repo_id, repo_type="space", revision=info.sha)) - actual - {".gitattributes"}
        for name in obsolete:
            if (Path(name).is_absolute() or ".." in Path(name).parts
                    or any(character in name for character in "*?[]")
                    or not (name.startswith(("assets/", "pages/", "sources/"))
                            or name in {"index.html", "source-index.html", "README.md", "hub-manifest.json", "style.css"})):
                raise ValueError("Refusing to remove an unrelated remote file during website sync")
        # Exact paths only, deleted atomically alongside additions. The parent
        # commit check prevents overwriting a concurrent remote edit.
        options["delete_patterns"] = sorted(obsolete)
    result = api.upload_folder(repo_id=repo_id, repo_type="space", folder_path=str(folder),
                               commit_message="Publish recipe and evidence hub", parent_commit=info.sha,
                               **options)
    verified = api.repo_info(repo_id, repo_type="space", revision=result.oid)
    if not _repository_hidden(verified, visibility) or verified.sha != result.oid:
        raise RuntimeError("Remote private visibility or commit verification failed")
    return {"repo_id": repo_id, "repo_type": "space", "private": True,
            "visibility": visibility, "commit": result.oid, "url": "https://huggingface.co/spaces/" + repo_id,
            "build_manifest_sha256": _sha((folder / "hub-manifest.json").read_bytes())}


def publish_collection(space_receipt: dict, *, collection_slug: str | None = None,
                       expected_owner: str = "ctauchmann", api=None) -> dict:
    """Create a private, links-only companion after the private Space is verified.

    This does not create or copy model/dataset repositories, change a profile, or
    change any existing collection's visibility. A receipt can resume a partial
    private collection by passing its slug explicitly.
    """
    repo_id = expected_owner + "/hessian-recipes"
    if (space_receipt.get("repo_id") != repo_id or space_receipt.get("private") is not True
            or not space_receipt.get("commit")):
        raise ValueError("A verified private recipe Space receipt is required")
    if api is None:
        from huggingface_hub import HfApi
        api = HfApi()
    if api.whoami().get("name") != expected_owner:
        raise ValueError("Authenticated Hub identity differs from the approved owner")
    space = api.repo_info(repo_id, repo_type="space", revision=space_receipt["commit"])
    if not _private_space(space) or space.id != repo_id or space.sha != space_receipt["commit"]:
        raise ValueError("The recipe Space must remain private at the verified commit")
    if collection_slug:
        collection = api.get_collection(collection_slug)
    else:
        collection = api.create_collection(
            "Hessian recipes — private research workspace", namespace=expected_owner,
            description="Recipe pages, executable source and aggregate evidence; upstream model/data references only. No trained artifacts or raw runs are uploaded.",
            private=True, exists_ok=False)
    if (_collection_owner(collection) != expected_owner or collection.private is not True
            or not collection.slug.startswith(expected_owner + "/")):
        raise ValueError("Refusing to edit a public or differently owned collection")
    expected = {(item_id, item_type) for item_id, item_type, _ in COLLECTION_ITEMS}
    if not {(item.item_id, item.item_type) for item in collection.items}.issubset(expected):
        raise ValueError("The selected collection contains unrelated entries")
    slug = collection.slug
    for item_id, item_type, note in COLLECTION_ITEMS:
        api.add_collection_item(slug, item_id=item_id, item_type=item_type, note=note, exists_ok=True)
    verified = api.get_collection(slug)
    if (_collection_owner(verified) != expected_owner or verified.private is not True
            or {(item.item_id, item.item_type) for item in verified.items} != expected
            or {(item.item_id, item.item_type): item.note for item in verified.items}
               != {(item_id, item_type): note for item_id, item_type, note in COLLECTION_ITEMS}):
        raise RuntimeError("Private collection inventory or visibility verification failed")
    return {"slug": slug, "url": "https://huggingface.co/collections/" + slug,
            "private": True, "owner": expected_owner, "items": [
                {"id": item_id, "type": item_type, "note": note} for item_id, item_type, note in COLLECTION_ITEMS],
            "copies_models_or_datasets": False, "space_commit": space_receipt["commit"]}


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--repo", type=Path, default=Path.cwd())
    parser.add_argument("--output", type=Path, required=True)
    parser.add_argument("--publish", metavar="OWNER/SPACE")
    parser.add_argument("--visibility", choices=("private", "protected"), default="private")
    args = parser.parse_args()
    result = build(args.repo, args.output, visibility=args.visibility)
    if args.publish:
        result = publish(args.output, args.publish, visibility=args.visibility)
    if not args.publish:
        result = {key: result[key] for key in ("source_commit", "recipe_count", "page_count", "source_file_count",
                                               "source_bytes", "long_experiment_status", "link_validation")}
        result["manifest"] = str(args.output / "hub-manifest.json")
    print(json.dumps(result, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    main()
