#!/usr/bin/env python3
"""Volume XXII — 99 theories in nine mechanism families. Entry page nave.html, never index.html."""
import html, json, hashlib, re, shutil
from datetime import datetime, timezone
from pathlib import Path

from data_v22_1 import THEORIES as A
from data_v22_2 import THEORIES as B
from data_v22_3 import THEORIES as C

THEORIES = A + B + C
SITE_BASE = "https://incoprea.com/theories-vol22"
ENTRY = "refectory.html"
BUILT = "30 October 2026"
ROOT = Path(__file__).resolve().parent
OUT = ROOT.parent / "out"
esc = html.escape

FIELDS = []
for t in THEORIES:
    if t["field"] not in FIELDS:
        FIELDS.append(t["field"])


def validate():
    assert len(THEORIES) == 99, f"expected 99, got {len(THEORIES)}"
    assert len({t["slug"] for t in THEORIES}) == 99, "duplicate slug"
    assert [t["n"] for t in THEORIES] == list(range(1, 100)), "numbering broken"
    assert len(FIELDS) == 9, "expected nine length scales"
    assert all(sum(1 for t in THEORIES if t["field"] == f) >= 10 for f in FIELDS), "at least ten per scale"
    for t in THEORIES:
        for k in ("title", "claim", "mechanism", "prediction", "refute", "field", "subject"):
            assert t.get(k), f"{t['slug']} missing {k}"


CSS = """
:root{--paper:#f2f3f0;--ink:#12150f;--muted:#68705f;--rule:#d3d7cc;--accent:#8a4a2a;--panel:#fafbf7}
*{box-sizing:border-box}
body{margin:0;background:var(--paper);color:var(--ink);
 font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;font-size:17px;line-height:1.6}
a{color:var(--accent);text-decoration:none;border-bottom:1px solid rgba(138,74,42,.3)}
.wrap{max-width:43rem;margin:0 auto;padding:0 1.3rem}
body.index .wrap{max-width:62rem}
.masthead{border-bottom:1px solid var(--rule);position:sticky;top:0;z-index:5;background:rgba(242,243,240,.96)}
.masthead .wrap{display:flex;justify-content:space-between;align-items:baseline;padding:.75rem 1.3rem;max-width:62rem}
.wordmark{font-family:ui-sans-serif,-apple-system,Helvetica,Arial,sans-serif;font-size:.68rem;letter-spacing:.24em;
 text-transform:uppercase;font-weight:600;border:0;color:var(--ink)}
.wordmark i{font-style:normal;color:var(--accent)}
.masthead .meta{font-family:ui-monospace,Menlo,monospace;font-size:.62rem;letter-spacing:.1em;
 text-transform:uppercase;color:var(--muted)}
.kicker{font-family:ui-monospace,Menlo,monospace;font-size:.63rem;letter-spacing:.13em;text-transform:uppercase;
 color:var(--muted);margin:2.2rem 0 .7rem;display:flex;gap:.5rem;flex-wrap:wrap}
.kicker b{color:var(--accent)}
.kicker .sep{color:var(--rule)}
h1{font-size:clamp(1.85rem,4.6vw,2.7rem);line-height:1.08;letter-spacing:-.018em;margin:.2rem 0 .5rem;font-weight:600}
.claim{border-left:3px solid var(--accent);background:var(--panel);padding:.9rem 1.1rem;margin:1.3rem 0 1.9rem}
.claim .lab{display:block;font-family:ui-monospace,Menlo,monospace;font-size:.57rem;letter-spacing:.2em;
 text-transform:uppercase;color:var(--accent);margin-bottom:.4rem}
.claim p{margin:0;font-size:1rem;line-height:1.5}
h2{font-family:ui-sans-serif,-apple-system,Helvetica,Arial,sans-serif;font-size:.67rem;letter-spacing:.18em;
 text-transform:uppercase;margin:2rem 0 .55rem;display:flex;gap:.6rem;align-items:baseline}
h2 .num{font-family:ui-monospace,Menlo,monospace;color:var(--accent);font-weight:400;font-size:.64rem}
h2:after{content:"";flex:1;height:1px;background:var(--rule);transform:translateY(-.2em)}
section p{margin:0 0 .85rem}
.pager{display:flex;justify-content:space-between;gap:1rem;border-top:1px solid var(--rule);
 margin-top:2.6rem;padding:1.1rem 0;font-size:.83rem}
.pager a{border:0}
.pager .mid{font-family:ui-monospace,Menlo,monospace;font-size:.62rem;letter-spacing:.1em;
 text-transform:uppercase;color:var(--muted)}
footer{border-top:1px solid var(--rule);padding:1.3rem 0 2.8rem;font-size:.76rem;color:var(--muted)}
footer strong{color:var(--ink)}
.hero{padding:2.7rem 0 1.9rem;border-bottom:1px solid var(--rule)}
.eyebrow{font-family:ui-monospace,Menlo,monospace;font-size:.64rem;letter-spacing:.18em;text-transform:uppercase;
 color:var(--accent);margin:0 0 .9rem}
.hero h1{font-size:clamp(2.1rem,5.2vw,3.4rem);max-width:22ch;margin:0 0 1rem}
.lede{font-size:1.08rem;line-height:1.5;color:#3a4034;max-width:40rem;margin:0 0 1.1rem}
.stats{display:flex;flex-wrap:wrap;gap:1.8rem;font-family:ui-monospace,Menlo,monospace;font-size:.6rem;
 letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.stats b{display:block;font-family:"Iowan Old Style",Palatino,Georgia,serif;font-size:1.5rem;letter-spacing:0;
 color:var(--ink);font-weight:600}
.famhead{display:flex;align-items:baseline;gap:1rem;margin:2.1rem 0 .3rem;border-bottom:1px solid var(--rule);padding-bottom:.35rem}
.famhead h2{margin:0;font-size:.68rem;letter-spacing:.18em}
.famhead h2:after{display:none}
.famhead .count{font-family:ui-monospace,Menlo,monospace;font-size:.58rem;color:var(--muted);margin-left:auto}
.row{display:grid;grid-template-columns:2.4rem 1fr;gap:.9rem;padding:.55rem 0;border-bottom:1px solid rgba(211,215,204,.7)}
.row .n{font-family:ui-monospace,Menlo,monospace;font-size:.7rem;color:var(--accent);padding-top:.2rem}
.row h3{margin:0 0 .1rem;font-size:1rem;line-height:1.25;font-weight:600}
.row h3 a{border:0;color:var(--ink)}
.row p{margin:0;font-size:.83rem;line-height:1.45;color:#4a5044}
.row .sub{font-family:ui-monospace,Menlo,monospace;font-size:.55rem;letter-spacing:.12em;text-transform:uppercase;
 color:var(--muted);margin-left:.4rem}
.colophon{border-top:1px solid var(--rule);margin-top:2.3rem;padding:1.8rem 0 3rem;font-size:.81rem;
 color:var(--muted);line-height:1.6}
.colophon p{max-width:42rem}
@media(max-width:640px){body{font-size:16px}.row{grid-template-columns:2rem 1fr;gap:.55rem}}
"""


def render_theory(t, prev, nxt, base=None, standalone=True):
    def href(slug):
        return f"{base}/{slug}/" if base else f"{slug}.html"

    pager = ('<nav class="pager">'
             + (f'<a href="{href(prev["slug"])}">&larr; {esc(prev["title"])}</a>' if prev else "<span></span>")
             + f'<a class="mid" href="../{ENTRY}">Contents</a>'
             + (f'<a href="{href(nxt["slug"])}">{esc(nxt["title"])} &rarr;</a>' if nxt else "<span></span>")
             + "</nav>")

    body = f"""<main class="wrap">
<article>
<p class="kicker"><b>Theory {t["n"]:02d}</b><span class="sep">/</span><span>{esc(t["field"])}</span><span class="sep">/</span><span>{esc(t["subject"])}</span></p>
<h1>{esc(t["title"])}</h1>
<div class="claim"><span class="lab">The claim</span><p>{esc(t["claim"])}</p></div>
<section><h2><span class="num">01</span>Mechanism</h2><p>{esc(t["mechanism"])}</p></section>
<section><h2><span class="num">02</span>Prediction</h2><p>{esc(t["prediction"])}</p></section>
<section><h2><span class="num">03</span>Refutation</h2><p>{esc(t["refute"])}</p></section>
</article>
{pager}
<footer><strong>Incoprea Theory, Volume XXII &middot; {t["n"]:02d} of 99.</strong>
Ninety-nine theories ordered by length scale. Written {BUILT}. Not peer reviewed.</footer>
</main>"""

    if not standalone:
        return body

    return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{esc(t["title"])} &middot; Incoprea Theory XXII</title>
<meta name="description" content="{esc(t["claim"][:157])}">
<link rel="canonical" href="{SITE_BASE}/theories/{t["slug"]}/">
<style>{CSS}</style></head>
<body>
<header class="masthead"><div class="wrap">
<a class="wordmark" href="../{ENTRY}">Incoprea <i>Theory</i></a>
<div class="meta">Vol. XXII &middot; 99</div>
</div></header>
{body}
</body></html>"""


def render_index():
    sections = []
    for f in FIELDS:
        items = [t for t in THEORIES if t["field"] == f]
        rows = "".join(
            f'<div class="row"><div class="n">{t["n"]:02d}</div><div>'
            f'<h3><a href="theories/{t["slug"]}.html">{esc(t["title"])}</a>'
            f'<span class="sub">{esc(t["subject"])}</span></h3>'
            f'<p>{esc(t["claim"])}</p></div></div>' for t in items)
        sections.append(f'<div class="famhead"><h2>{esc(f)}</h2><span class="count">{len(items)}</span></div>{rows}')

    return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Incoprea Theory, Volume XXII &middot; 99 theories by length scale</title>
<meta name="description" content="Ninety-nine original theories ordered by length scale, from molecular to cosmic.">
<link rel="canonical" href="{SITE_BASE}/{ENTRY}">
<style>{CSS}</style></head>
<body class="index">
<header class="masthead"><div class="wrap">
<a class="wordmark" href="{ENTRY}">Incoprea <i>Theory</i></a>
<div class="meta">Volume XXII &middot; {BUILT}</div>
</div></header>

<section class="hero"><div class="wrap">
<p class="eyebrow">Nine length scales &middot; eleven claims each &middot; not peer reviewed</p>
<h1>Ninety-nine theories, sorted by size</h1>
<p class="lede">Nine length scales, from a single binding pocket to the cosmic web. Where Volume XIX sorted by clock, this one sorts by ruler — and makes the same bet, that a governing ratio transfers across twenty-five orders of magnitude.</p>
<div class="stats"><div><b>99</b>theories</div><div><b>9</b>scales</div><div><b>99</b>predictions</div><div><b>99</b>refutations</div></div>
</div></section>

<div class="wrap">
{''.join(sections)}
<div class="colophon">
<h2>Why sort by mechanism</h2>
<p>A field-by-field catalogue tells you what each discipline thinks. A mechanism-by-mechanism catalogue
tells you what kind of argument a discipline is making, and lets you check whether the same argument
holds somewhere else. Each family here is a distinct causal shape; the eleven instances test whether
it generalises.</p>
<p>Entry page: <code>{ENTRY}</code>. There is deliberately no <code>index.html</code>.</p>
</div>
</div>
</body></html>"""


def render_wp(t):
    body = render_theory(t, None, None, base=SITE_BASE, standalone=False)
    body = re.sub(r'<nav class="pager">.*?</nav>', "", body, flags=re.S)
    body = body.replace('<main class="wrap">', "").replace("</main>", "")
    return f"""<!-- Incoprea Theory XXII · theory {t["n"]:02d} · WordPress Custom HTML -->
<article class="incoprea-theory-xvi" style="max-width:43rem;margin:0 auto;font-family:'Iowan Old Style',Palatino,Georgia,serif;line-height:1.6">
{body}
</article>
"""


README = f"""# Incoprea Theory, Volume XXII — 99 theories by mechanism

Built {BUILT}. **Entry page: `{ENTRY}`.** There is no `index.html`.

Fifteen volumes sorted theories by field. This one sorts them by mechanism: nine families of causal
shape, eleven instances each. The claim is that the families transfer across disciplines.

## Contents

| Path | What |
|---|---|
| `{ENTRY}` | Contents page — the entry point |
| `theories/<slug>.html` | One page per theory (99) |
| `wordpress/<slug>.html` | WordPress fragment per theory |
| `wordpress/theory-index.html` | Contents fragment |
| `MANIFEST.json` | SHA-256 for every file |
| `build/` | Three data files and the renderer |

## Families

""" + "\n".join(f"- {f} (11)" for f in FIELDS) + f"""

## Format

Claim, mechanism, one prediction, refutation. The shortest format in the series, because the point is
the shape of the argument rather than the detail of the field.

## Rebuilding

Run `python3 build_v22.py` from `build/` to regenerate `out/`.
"""

PUBLISH = f"""# Publish order — Volume XXII

## Static

1. Upload to `{SITE_BASE}/`.
2. Entry page is `{ENTRY}`, not `index.html`. Link to `{SITE_BASE}/{ENTRY}`, or set the server default
   document to `{ENTRY}`.
3. `theories/*.html` are the ninety-nine article pages.

## WordPress

1. Parent page at `/theories/`, paste `wordpress/theory-index.html` into a Custom HTML block.
2. Child page per theory, slug = filename, paste `wordpress/<slug>.html`.
3. Cross-links are absolute to `{SITE_BASE}/`.
"""


def sha(p):
    return hashlib.sha256(p.read_bytes()).hexdigest()


def main():
    validate()
    if OUT.exists():
        shutil.rmtree(OUT)
    (OUT / "theories").mkdir(parents=True)
    (OUT / "wordpress").mkdir(parents=True)

    for i, t in enumerate(THEORIES):
        prev = THEORIES[i - 1] if i > 0 else None
        nxt = THEORIES[i + 1] if i < len(THEORIES) - 1 else None
        (OUT / "theories" / f'{t["slug"]}.html').write_text(render_theory(t, prev, nxt), encoding="utf-8")
        (OUT / "wordpress" / f'{t["slug"]}.html').write_text(render_wp(t), encoding="utf-8")

    (OUT / ENTRY).write_text(render_index(), encoding="utf-8")

    blocks = []
    for f in FIELDS:
        items = [t for t in THEORIES if t["field"] == f]
        rows = "".join(f'<li><a href="{SITE_BASE}/theories/{t["slug"]}/">{esc(t["title"])}</a> &mdash; {esc(t["claim"])}</li>' for t in items)
        blocks.append(f"<h3>{esc(f)}</h3><ol>{rows}</ol>")
    frag = ('<!-- Incoprea Theory XXII · contents · WordPress Custom HTML -->\n'
            '<div style="font-family:\'Iowan Old Style\',Palatino,Georgia,serif;max-width:43rem;margin:0 auto">\n'
            '<p style="font-style:italic">Ninety-nine theories in nine length scales. Not peer reviewed.</p>\n'
            + "".join(blocks) + "</div>\n")
    (OUT / "wordpress" / "theory-index.html").write_text(frag, encoding="utf-8")

    (OUT / "README.md").write_text(README, encoding="utf-8")
    (OUT / "PUBLISH-ORDER.md").write_text(PUBLISH, encoding="utf-8")

    src = OUT / "build"
    src.mkdir(exist_ok=True)
    for f in ("build_v22.py", "data_v22_1.py", "data_v22_2.py", "data_v22_3.py"):
        shutil.copy2(ROOT / f, src / f)

    files = {str(p.relative_to(OUT)): {"sha256": sha(p), "bytes": p.stat().st_size}
             for p in sorted(OUT.rglob("*")) if p.is_file() and p.name != "MANIFEST.json"}
    words = sum(len((t["claim"] + " " + t["mechanism"] + " " + t["prediction"] + " " + t["refute"]).split()) for t in THEORIES)
    manifest = {"bundle": "Incoprea Theory, Volume XXII", "built": BUILT,
                "generated_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
                "site_base": SITE_BASE, "entry": ENTRY, "theories": len(THEORIES),
                "families": {f: sum(1 for t in THEORIES if t["field"] == f) for f in FIELDS},
                "words": words, "files": files, "file_count": len(files)}
    (OUT / "MANIFEST.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
    print(f"theories: {len(THEORIES)}  families: {len(FIELDS)}  words: {words:,}  files: {len(files)}  entry: {ENTRY}")


if __name__ == "__main__":
    main()
