#!/usr/bin/env python3
"""Volume XXVIII — nine developed theories. Entry page garth.html, never index.html."""
import html, json, hashlib, re, shutil
from datetime import datetime, timezone
from pathlib import Path

from data_v28 import THEORIES

SITE_BASE = "https://incoprea.com/theories-vol28"
ENTRY = "garth.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) == 9, f"expected 9, got {len(THEORIES)}"
    assert len({t["slug"] for t in THEORIES}) == 9, "duplicate slug"
    assert [t["n"] for t in THEORIES] == list(range(1, 10)), "numbering broken"
    for t in THEORIES:
        for k in ("title", "claim", "background", "mechanism", "refute", "test", "field", "subject"):
            assert t.get(k), f"{t['slug']} missing {k}"
        assert len(t["predictions"]) == 3, f"{t['slug']} needs three predictions"
        assert len(t["claim"].split()) > 25, f"{t['slug']} claim too short for this volume"


CSS = """
:root{--paper:#f4f1ea;--ink:#14140f;--muted:#6b6a5c;--rule:#d8d3c5;--accent:#6b5a1f;--panel:#faf8f1;--tint:#f0ece0}
*{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:18px;line-height:1.66}
a{color:var(--accent);text-decoration:none;border-bottom:1px solid rgba(107,90,31,.32)}
.wrap{max-width:49rem;margin:0 auto;padding:0 1.4rem}
body.index .wrap{max-width:60rem}
.masthead{border-bottom:1px solid var(--rule);background:rgba(244,241,234,.96);position:sticky;top:0;z-index:5}
.masthead .wrap{display:flex;justify-content:space-between;align-items:baseline;padding:.8rem 1.4rem;max-width:60rem}
.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.6rem 0 .8rem;display:flex;gap:.55rem;flex-wrap:wrap}
.kicker b{color:var(--accent)}
.kicker .sep{color:var(--rule)}
h1{font-size:clamp(2rem,4.8vw,2.95rem);line-height:1.06;letter-spacing:-.02em;margin:.2rem 0 1.4rem;font-weight:600}
.claim{border-left:3px solid var(--accent);background:var(--panel);padding:1.15rem 1.35rem;margin:0 0 2.2rem}
.claim .lab{display:block;font-family:ui-monospace,Menlo,monospace;font-size:.57rem;letter-spacing:.2em;
 text-transform:uppercase;color:var(--accent);margin-bottom:.5rem}
.claim p{margin:0;font-size:1.12rem;line-height:1.52}
h2{font-family:ui-sans-serif,-apple-system,Helvetica,Arial,sans-serif;font-size:.68rem;letter-spacing:.18em;
 text-transform:uppercase;margin:2.4rem 0 .7rem;display:flex;gap:.7rem;align-items:baseline;color:var(--ink)}
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(-.22em)}
section p{margin:0 0 1rem}
section p.drop:first-letter{float:left;font-size:3.2rem;line-height:.84;padding:.12rem .5rem 0 0;color:var(--accent);font-weight:600}
ol.preds{padding-left:0;margin:0;list-style:none;counter-reset:p}
ol.preds li{counter-increment:p;position:relative;padding-left:2.1rem;margin-bottom:.95rem;font-size:1rem;line-height:1.55}
ol.preds li:before{content:"P" counter(p);position:absolute;left:0;top:.18rem;font-family:ui-monospace,Menlo,monospace;
 font-size:.66rem;color:var(--accent);letter-spacing:.06em}
.testbox{background:var(--tint);border:1px solid var(--rule);padding:1.15rem 1.3rem;margin:1.6rem 0 0}
.testbox .lab{display:block;font-family:ui-monospace,Menlo,monospace;font-size:.57rem;letter-spacing:.2em;
 text-transform:uppercase;color:var(--accent);margin-bottom:.5rem}
.testbox p{margin:0;font-size:.98rem;line-height:1.58}
.pager{display:flex;justify-content:space-between;gap:1rem;border-top:1px solid var(--rule);
 margin-top:2.8rem;padding:1.15rem 0;font-size:.84rem}
.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.4rem 0 3rem;font-size:.78rem;color:var(--muted)}
footer strong{color:var(--ink)}
.hero{padding:3rem 0 2rem;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 1rem}
.hero h1{font-size:clamp(2.2rem,5vw,3.3rem);max-width:26ch;margin:0 0 1.1rem}
.lede{font-size:1.12rem;line-height:1.55;color:#3f4033;max-width:42rem;margin:0 0 1.3rem}
.stats{display:flex;flex-wrap:wrap;gap:2rem;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.6rem;letter-spacing:0;
 color:var(--ink);font-weight:600}
.item{display:grid;grid-template-columns:2.6rem 1fr;gap:1.1rem;padding:1.15rem 0;border-bottom:1px solid var(--rule)}
.item .n{font-family:ui-monospace,Menlo,monospace;font-size:.78rem;color:var(--accent);padding-top:.25rem}
.item h3{margin:0 0 .3rem;font-size:1.18rem;line-height:1.25;font-weight:600}
.item h3 a{border:0;color:var(--ink)}
.item .sub{display:block;font-family:ui-monospace,Menlo,monospace;font-size:.55rem;letter-spacing:.13em;
 text-transform:uppercase;color:var(--muted);margin-top:.35rem}
.item p{margin:.35rem 0 0;font-size:.94rem;line-height:1.5;color:#4b4c3e}
.colophon{border-top:1px solid var(--rule);margin-top:2.4rem;padding:1.9rem 0 3.2rem;font-size:.85rem;
 color:var(--muted);line-height:1.65}
.colophon h2{margin-top:0}
.colophon p{max-width:44rem}
@media(max-width:640px){body{font-size:17px}.item{grid-template-columns:2rem 1fr;gap:.7rem}}
"""


def render_theory(t, prev, nxt):
    preds = "".join(f"<li>{esc(p)}</li>" for p in t["predictions"])
    pager = ('<nav class="pager">'
             + (f'<a href="{prev["slug"]}.html">&larr; {esc(prev["title"])}</a>' if prev else "<span></span>")
             + f'<a class="mid" href="../{ENTRY}">Contents</a>'
             + (f'<a href="{nxt["slug"]}.html">{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["subject"])}</span><span class="sep">/</span><span>{esc(t["field"])}</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>What is already known</h2><p class="drop">{esc(t["background"])}</p></section>
<section><h2><span class="num">02</span>The mechanism</h2><p>{esc(t["mechanism"])}</p></section>
<section><h2><span class="num">03</span>Predictions</h2><ol class="preds">{preds}</ol></section>
<section><h2><span class="num">04</span>Refutation</h2><p>{esc(t["refute"])}</p></section>
<div class="testbox"><span class="lab">How it could be tested</span><p>{esc(t["test"])}</p></div>
</article>
{pager}
<footer><strong>Incoprea Theory, Volume XXVIII &middot; {t["n"]:02d} of 9.</strong>
Nine theories, developed at length. Written {BUILT}. Not peer reviewed.</footer>
</main>"""

    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 XXVIII</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. XXVIII &middot; 9</div>
</div></header>
{body}
</body></html>"""


def render_index():
    items = "".join(
        f'<div class="item"><div class="n">{t["n"]:02d}</div><div>'
        f'<h3><a href="theories/{t["slug"]}.html">{esc(t["title"])}</a></h3>'
        f'<p>{esc(t["claim"])}</p>'
        f'<span class="sub">{esc(t["subject"])} &middot; {esc(t["field"])}</span></div></div>' for t in THEORIES)

    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 XXVIII &middot; nine theories, developed</title>
<meta name="description" content="Nine original theories developed at length: each with background, mechanism, three predictions, a refutation and a test.">
<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 XXVIII &middot; {BUILT}</div>
</div></header>

<section class="hero"><div class="wrap">
<p class="eyebrow">Nine theories &middot; developed at length &middot; not peer reviewed</p>
<h1>Nine theories, argued rather than asserted</h1>
<p class="lede">The earlier volumes compressed. This one does the opposite: nine theories, each given a background,
a mechanism, three predictions, a refutation and a way it could actually be tested. Fewer claims, longer arguments.</p>
<div class="stats"><div><b>9</b>theories</div><div><b>27</b>predictions</div><div><b>9</b>refutations</div><div><b>9</b>tests</div></div>
</div></section>

<div class="wrap">
{items}
<div class="colophon">
<h2>Why fewer, and longer</h2>
<p>At one claim per paragraph, a theory is a slogan. These nine are given room to be wrong in specific ways — each
has three predictions that could come out differently, a refutation that would end it, and a test that some
existing body of data could already settle.</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)
    inner = body.split('<main class="wrap">')[1].split('</main>')[0]
    inner = re.sub(r'<nav class="pager">.*?</nav>', "", inner, flags=re.S)
    inner = inner.replace("../" + ENTRY, SITE_BASE + "/" + ENTRY)
    inner = re.sub(r'href="(?!http)([a-z0-9\-]+)\.html"', lambda m: f'href="{SITE_BASE}/theories/{m.group(1)}/"', inner)
    return f"""<!-- Incoprea Theory XXVIII · theory {t["n"]:02d} · WordPress Custom HTML -->
<article class="incoprea-theory-xxviii" style="max-width:49rem;margin:0 auto;font-family:'Iowan Old Style',Palatino,Georgia,serif;font-size:18px;line-height:1.66;color:#14140f">
{inner}
</article>
"""


README = f"""# Incoprea Theory, Volume XXVIII — nine developed theories

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

Nine theories, each with background, mechanism, three predictions, a refutation and a test. The
series' earlier volumes compressed; this one argues.

## Contents

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

## The nine

""" + "\n".join(f"{t['n']}. **{t['title']}** — {t['subject']}" for t in THEORIES) + """

## Rebuilding

`python3 build_v28.py` from `build/` regenerates `out/`.
"""

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

1. Upload to `{SITE_BASE}/`.
2. Entry page is `{ENTRY}`, not `index.html`.
3. `theories/*.html` are the nine article pages.
4. WordPress: paste `wordpress/<slug>.html` per page; parent index is `wordpress/theory-index.html`.
"""


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")

    rows = "".join(f'<li><a href="{SITE_BASE}/theories/{t["slug"]}/">{esc(t["title"])}</a> &mdash; {esc(t["claim"])}</li>' for t in THEORIES)
    frag = ('<!-- Incoprea Theory XXVIII · contents · WordPress Custom HTML -->\n'
            '<div style="font-family:\'Iowan Old Style\',Palatino,Georgia,serif;max-width:49rem;margin:0 auto">\n'
            '<p style="font-style:italic">Nine theories, developed at length. Not peer reviewed.</p>\n'
            f'<ol>{rows}</ol></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_v28.py", "data_v28.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["background"] + " " + t["mechanism"] + " " + t["refute"] + " " + t["test"]).split()) for t in THEORIES)
    manifest = {"bundle": "Incoprea Theory, Volume XXVIII", "built": BUILT,
                "generated_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
                "site_base": SITE_BASE, "entry": ENTRY, "theories": len(THEORIES),
                "predictions": sum(len(t["predictions"]) for t in THEORIES), "words": words,
                "fields": {f: sum(1 for t in THEORIES if t["field"] == f) for f in FIELDS},
                "files": files, "file_count": len(files)}
    (OUT / "MANIFEST.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
    print(f"theories: {len(THEORIES)}  predictions: {manifest['predictions']}  words: {words:,}  files: {len(files)}  entry: {ENTRY}")


if __name__ == "__main__":
    main()
