#!/usr/bin/env python3
"""Volume XII — 29 theories, 29 physical-science subfields. Entry page stoa.html, never index.html."""
import html, json, hashlib, re, shutil
from datetime import datetime, timezone
from pathlib import Path

from data_v12 import THEORIES

SITE_BASE = "https://incoprea.com/theories-vol12"
ENTRY = "stoa.html"
BUILT = "30 October 2026"
ROOT = Path(__file__).resolve().parent
OUT = ROOT.parent / "out"
esc = html.escape

FIELDS = [t["field"] for t in THEORIES]


def validate():
    assert len(THEORIES) == 29, f"expected 29, got {len(THEORIES)}"
    assert len({t["slug"] for t in THEORIES}) == 29, "duplicate slug"
    assert [t["n"] for t in THEORIES] == list(range(1, 30)), "numbering broken"
    assert len(set(FIELDS)) == 29, "expected 29 distinct fields"
    for t in THEORIES:
        for k in ("title", "claim", "mechanism", "refute", "field"):
            assert t.get(k), f"{t['slug']} missing {k}"
        assert len(t["predictions"]) == 2


CSS = """
:root{--paper:#f3f2ee;--ink:#121517;--muted:#66707a;--rule:#d5d7d6;--accent:#1a5f7a;--panel:#fafaf7}
*{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.6}
a{color:var(--accent);text-decoration:none;border-bottom:1px solid rgba(26,95,122,.3)}
.wrap{max-width:44rem;margin:0 auto;padding:0 1.35rem}
body.index .wrap{max-width:60rem}
.masthead{border-bottom:1px solid var(--rule);position:sticky;top:0;z-index:5;background:rgba(243,242,238,.96)}
.masthead .wrap{display:flex;justify-content:space-between;align-items:baseline;padding:.78rem 1.35rem;max-width:60rem}
.wordmark{font-family:ui-sans-serif,-apple-system,Helvetica,Arial,sans-serif;font-size:.7rem;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:.64rem;letter-spacing:.14em;text-transform:uppercase;
 color:var(--muted);margin:2.4rem 0 .8rem;display:flex;gap:.5rem;flex-wrap:wrap}
.kicker b{color:var(--accent)}
.kicker .sep{color:var(--rule)}
h1{font-size:clamp(1.95rem,4.8vw,2.85rem);line-height:1.07;letter-spacing:-.018em;margin:.2rem 0 .6rem;font-weight:600}
.claim{border-left:3px solid var(--accent);background:var(--panel);padding:.95rem 1.15rem;margin:1.5rem 0 2.1rem}
.claim .lab{display:block;font-family:ui-monospace,Menlo,monospace;font-size:.58rem;letter-spacing:.2em;
 text-transform:uppercase;color:var(--accent);margin-bottom:.45rem}
.claim p{margin:0;font-size:1.02rem;line-height:1.5}
h2{font-family:ui-sans-serif,-apple-system,Helvetica,Arial,sans-serif;font-size:.68rem;letter-spacing:.18em;
 text-transform:uppercase;margin:2.3rem 0 .65rem;display:flex;gap:.6rem;align-items:baseline}
h2 .num{font-family:ui-monospace,Menlo,monospace;color:var(--accent);font-weight:400;font-size:.65rem}
h2:after{content:"";flex:1;height:1px;background:var(--rule);transform:translateY(-.2em)}
section p{margin:0 0 .9rem}
.preds{list-style:none;padding:0 0 0 1.05rem;margin:0;border-left:2px solid var(--accent)}
.preds li{font-size:.95rem;margin-bottom:.65rem;line-height:1.5}
.preds .tag{font-family:ui-monospace,Menlo,monospace;font-size:.6rem;font-weight:700;margin-right:.45rem;color:var(--accent)}
.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.35rem 0 2.8rem;font-size:.77rem;color:var(--muted)}
footer strong{color:var(--ink)}
.hero{padding:2.9rem 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 .9rem}
.hero h1{font-size:clamp(2.2rem,5.2vw,3.5rem);max-width:21ch;margin:0 0 1rem}
.lede{font-size:1.1rem;line-height:1.5;color:#3b4148;max-width:40rem;margin:0 0 1.2rem}
.stats{display:flex;flex-wrap:wrap;gap:1.8rem;font-family:ui-monospace,Menlo,monospace;font-size:.61rem;
 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}
.entry{display:grid;grid-template-columns:2.8rem 1fr;gap:1rem;padding:.85rem 0;border-bottom:1px solid var(--rule)}
.entry .n{font-family:ui-monospace,Menlo,monospace;font-size:.74rem;color:var(--accent);padding-top:.25rem}
.entry .field{font-family:ui-monospace,Menlo,monospace;font-size:.56rem;letter-spacing:.14em;text-transform:uppercase;
 color:var(--muted);display:block;margin-bottom:.15rem}
.entry h3{margin:0 0 .15rem;font-size:1.05rem;line-height:1.25;font-weight:600}
.entry h3 a{border:0;color:var(--ink)}
.entry p{margin:0;font-size:.86rem;line-height:1.45;color:#495058}
.colophon{border-top:1px solid var(--rule);margin-top:2.5rem;padding:1.9rem 0 3rem;font-size:.82rem;
 color:var(--muted);line-height:1.6}
.colophon p{max-width:42rem}
@media(max-width:640px){body{font-size:17px}.entry{grid-template-columns:2rem 1fr;gap:.6rem}}
"""


def render_theory(t, prev, nxt, base=None, standalone=True):
    preds = "".join(f'<li><span class="tag">P{i}</span>{esc(txt)}</li>' for i, txt in enumerate(t["predictions"], 1))

    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></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>The mechanism</h2><p>{esc(t["mechanism"])}</p></section>
<section><h2><span class="num">02</span>Predictions</h2><ul class="preds">{preds}</ul></section>
<section><h2><span class="num">03</span>Refutation</h2><p>{esc(t["refute"])}</p></section>
</article>
{pager}
<footer><strong>Incoprea Theory, Volume XII &middot; {t["n"]:02d} of 29.</strong>
One theory from each of twenty-nine physical-science subfields. 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 XII</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. XII &middot; 29 subfields</div>
</div></header>
{body}
</body></html>"""


def render_index():
    entries = "".join(
        f'<div class="entry"><div class="n">{t["n"]:02d}</div><div>'
        f'<span class="field">{esc(t["field"])}</span>'
        f'<h3><a href="theories/{t["slug"]}.html">{esc(t["title"])}</a></h3>'
        f'<p>{esc(t["claim"])}</p></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 XII &middot; 29 subfields</title>
<meta name="description" content="Twenty-nine original theories, one from each of twenty-nine physical-science subfields, each with a mechanism and a refutation.">
<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 XII &middot; {BUILT}</div>
</div></header>

<section class="hero"><div class="wrap">
<p class="eyebrow">Twenty-nine subfields &middot; one claim each &middot; not peer reviewed</p>
<h1>Twenty-nine theories from the physical sciences</h1>
<p class="lede">The earlier volumes leaned biological, earthly and social. This one goes to the
physical sciences: from magnetic reconnection to metrology, one claim per subfield, each with a
mechanism and a stated refutation.</p>
<div class="stats"><div><b>29</b>theories</div><div><b>29</b>subfields</div><div><b>58</b>predictions</div><div><b>29</b>refutations</div></div>
</div></section>

<div class="wrap">
{entries}
<div class="colophon">
<h2>Why this set</h2>
<p>Twenty-nine subfields that the series had either skipped or treated as background. The entries are
short by design: a claim, the mechanism that would make it true, two predictions, and the result that
kills it. No bibliography; the evidence is the standard picture of each subfield rather than a
specific paper.</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 XII · theory {t["n"]:02d} · WordPress Custom HTML -->
<article class="incoprea-theory-xii" style="max-width:44rem;margin:0 auto;font-family:'Iowan Old Style',Palatino,Georgia,serif;line-height:1.6">
{body}
</article>
"""


README = f"""# Incoprea Theory, Volume XII — 29 theories, 29 subfields

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

Twenty-nine original theories, one from each of twenty-nine physical-science subfields the series had
not treated on its own. The earlier volumes leaned biological, earthly and social; this one is physics,
chemistry and the physical earth.

## Contents

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

## Subfields

""" + "\n".join(f"{t['n']:02d}. {t['field']} — {t['title']}" for t in THEORIES) + f"""

## Format

Claim, mechanism, two predictions, refutation. No bibliography; the evidence invoked is the standard
picture of each subfield.

## Rebuilding

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

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

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

    rows = "".join(f'<li><a href="{SITE_BASE}/theories/{t["slug"]}/">{esc(t["title"])}</a> &mdash; <em>{esc(t["field"])}</em> — {esc(t["claim"])}</li>' for t in THEORIES)
    frag = ('<!-- Incoprea Theory XII · contents · WordPress Custom HTML -->\n'
            '<div style="font-family:\'Iowan Old Style\',Palatino,Georgia,serif;max-width:44rem;margin:0 auto">\n'
            '<p style="font-style:italic">Twenty-nine theories from twenty-nine physical-science subfields. 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_v12.py", "data_v12.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["refute"] + " " + " ".join(t["predictions"])).split()) for t in THEORIES)
    manifest = {"bundle": "Incoprea Theory, Volume XII", "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,
                "subfields": len(set(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)}  subfields: {len(set(FIELDS))}  words: {words:,}  files: {len(files)}  entry: {ENTRY}")


if __name__ == "__main__":
    main()
