#!/usr/bin/env python3
"""Volume XV — 22 theories in 11 domains. Entry page transept.html, never index.html."""
import html, json, hashlib, re, shutil
from datetime import datetime, timezone
from pathlib import Path

from data_v15 import THEORIES

SITE_BASE = "https://incoprea.com/theories-vol15"
ENTRY = "transept.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) == 22, f"expected 22, got {len(THEORIES)}"
    assert len({t["slug"] for t in THEORIES}) == 22, "duplicate slug"
    assert [t["n"] for t in THEORIES] == list(range(1, 23)), "numbering broken"
    assert len(FIELDS) == 11, "expected eleven domains"
    assert all(sum(1 for t in THEORIES if t["field"] == f) == 2 for f in FIELDS), "two per domain"
    for t in THEORIES:
        for k in ("title", "claim", "refute", "field"):
            assert t.get(k), f"{t['slug']} missing {k}"
        assert len(t["mechanism"]) == 2
        assert len(t["predictions"]) == 2
        assert len(t["prior"]) == 2


CSS = """
:root{--paper:#f5f1e9;--ink:#14120e;--muted:#6e675a;--rule:#dad2c0;--accent:#8a5a2b;--panel:#fbf8f2}
*{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(138,90,43,.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(245,241,233,.96)}
.masthead .wrap{display:flex;justify-content:space-between;align-items:baseline;padding:.8rem 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.2rem 0 .6rem;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}
.not{border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:.85rem 0}
.not .prior{display:block;font-family:ui-monospace,Menlo,monospace;font-size:.6rem;letter-spacing:.1em;
 text-transform:uppercase;color:var(--muted);margin-bottom:.35rem}
.not p{margin:0;font-size:.95rem}
.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:.6rem;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.7rem;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:#3c382e;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}
.fieldhead{display:flex;align-items:baseline;gap:1rem;margin:2.3rem 0 .4rem;border-bottom:1px solid var(--rule);padding-bottom:.4rem}
.fieldhead h2{margin:0;font-size:.67rem;letter-spacing:.18em}
.fieldhead h2:after{display:none}
.fieldhead .count{font-family:ui-monospace,Menlo,monospace;font-size:.6rem;color:var(--muted);margin-left:auto}
.entry{display:grid;grid-template-columns:2.6rem 1fr;gap:1rem;padding:.8rem 0;border-bottom:1px solid rgba(218,210,192,.7)}
.entry .n{font-family:ui-monospace,Menlo,monospace;font-size:.73rem;color:var(--accent);padding-top:.25rem}
.entry h3{margin:0 0 .15rem;font-size:1.04rem;line-height:1.25;font-weight:600}
.entry h3 a{border:0;color:var(--ink)}
.entry p{margin:0;font-size:.85rem;line-height:1.45;color:#4b463a}
.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))
    paras = "".join(f"<p>{esc(p)}</p>" for p in t["mechanism"])
    prior_name, prior_diff = t["prior"]

    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>{paras}</section>
<section><h2><span class="num">02</span>What this is not</h2>
<div class="not"><span class="prior">{esc(prior_name)}</span><p>{esc(prior_diff)}</p></div></section>
<section><h2><span class="num">03</span>Predictions</h2><ul class="preds">{preds}</ul></section>
<section><h2><span class="num">04</span>Refutation</h2><p>{esc(t["refute"])}</p></section>
</article>
{pager}
<footer><strong>Incoprea Theory, Volume XV &middot; {t["n"]:02d} of 22.</strong>
Two theories in each of eleven clinical and applied domains. 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 XV</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. XV &middot; twenty-two</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="entry"><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></div></div>' for t in items)
        sections.append(f'<div class="fieldhead"><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 XV &middot; twenty-two theories</title>
<meta name="description" content="Twenty-two theories in eleven clinical and applied domains: space medicine, diving, altitude, toxicology, pharmacovigilance, emergency medicine, geriatrics, palliative care, prosthetics, phonetics, trial methodology.">
<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 XV &middot; {BUILT}</div>
</div></header>

<section class="hero"><div class="wrap">
<p class="eyebrow">Eleven domains &middot; two claims each &middot; not peer reviewed</p>
<h1>Twenty-two theories from applied medicine</h1>
<p class="lede">Eleven domains where a clinical decision is made on a level — a dose, a saturation, a
count, a pressure — and where the evidence keeps pointing at a ratio instead: signal to noise, load
to reserve, nucleation to elimination.</p>
<div class="stats"><div><b>22</b>theories</div><div><b>11</b>domains</div><div><b>44</b>predictions</div><div><b>22</b>refutations</div></div>
</div></section>

<div class="wrap">
{''.join(sections)}
<div class="colophon">
<h2>Why these domains</h2>
<p>Each is a setting where a decision rule is calibrated on a single measurement, and where the two
entries in the domain argue that the measurement is one term of a ratio. Space medicine and diving are
there because their extreme conditions make the ratios visible; geriatrics and palliative care because
the same arithmetic governs reserve.</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 XV · theory {t["n"]:02d} · WordPress Custom HTML -->
<article class="incoprea-theory-xv" 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 XV — 22 theories

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

Twenty-two theories in eleven clinical and applied domains, two each. Each entry carries a mechanism,
a differentiation from the incumbent model, two predictions and a refutation.

## Contents

| Path | What |
|---|---|
| `{ENTRY}` | Contents page — the entry point |
| `theories/<slug>.html` | One page per theory (22) |
| `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 |

## Domains

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

## The through-line

Every domain here makes a decision on a single level — a dose, a saturation, a report count, a
pressure — and each pair of claims argues that the decision should be made on a ratio instead.

## Rebuilding

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

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

## 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-two 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 XV · 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-two theories in eleven applied domains. 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_v15.py", "data_v15.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["refute"] + " " + " ".join(t["mechanism"]) + " " + " ".join(t["predictions"])).split()) for t in THEORIES)
    manifest = {"bundle": "Incoprea Theory, Volume XV", "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,
                "domains": len(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)}  domains: {len(FIELDS)}  words: {words:,}  files: {len(files)}  entry: {ENTRY}")


if __name__ == "__main__":
    main()
