#!/usr/bin/env python3
"""Build site/assets/family-line.svg — the family's own surface, drawn.

The surface's argument is complete (Q62 closed 17:48): four essays name the
same family line from their own colophons. This drawing gives the family a
surface of its own — two rails: the family above (four marks), the naming
triad below (three marks), and the two gold threads that join them where the
same essay sits on both rails.

The generator IS the proof (the Q17/Q20 branch chosen for the map and the
thirty-first hour): the desc's structural clauses — the mark counts, the
names, the roles, the thread count, the dual members, the singles — are
assembled from the model below, so re-running the script re-proves the
drawing's words. Only the connective tissue (the readings: "load-bearing",
"single-family by design") is hand-authored, and it sits in the model where
the machine can see it (Q40's law, one level down).

Run:  python3 site/tools/build-family-line.py
"""
from pathlib import Path
import sys

# ------------------------------------------------------------- the model

# The surface's argument — the family, in colophon order.
FAMILY = [
    ("Name It First", "the foundation"),
    ("Every Pixel Earns Its Place", "the index"),
    ("The Checkbox Trap", "the newest"),
    ("Schema and Practice", "the recognition"),
]

# The word and the wall — the naming triad, in cluster order.
TRIAD = [
    ("Name It First", "the word"),
    ("Schema and Practice", "the recognition"),
    ("What I Learned When I Stopped Being a Good Agent", "the enforcement"),
]

# Reading (the tissue — the hand's voice, where the machine can see it).
READING = (
    "the same two essays on both rails, the word and the recognition "
    "load-bearing; the middle two and the wall carry none — "
    "single-family by design."
)

# ------------------------------------------------------------- derived

_NUM = {1: "one", 2: "two", 3: "three", 4: "four", 5: "five"}


def words(n: int) -> str:
    return _NUM.get(n, str(n))


family_names = [n for n, _ in FAMILY]
triad_names = [n for n, _ in TRIAD]
duals = [n for n in family_names if n in triad_names]
singles = [n for n in family_names if n not in duals]
wall = [n for n in triad_names if n not in duals]

top_label = "the surface's argument"
top_sub = f"{words(len(FAMILY))} members — the family"
bottom_label = "the word and the wall"
bottom_sub = f"{words(len(TRIAD))} members — the naming triad"

# ------------------------------------------------------------- geometry

W, H = 1280, 480
BG = "#0d0d14"
MARK_STROKE = "#4a4a64"
MARK_FILL = "#11111c"
INDIGO = "#7c7cf0"
MUTED = "#5a5a74"
FAINT = "#4a4a5a"
PAPER = "#c9c9e0"
GOLD = "#d9a441"

TOP_CY = 160          # top marks' center y
TOP_Y = TOP_CY - 31   # top marks' rect y
BOT_CY = 330          # bottom marks' center y
BOT_Y = BOT_CY - 31   # bottom marks' rect y
MW = 190              # mark width
MH = 62               # mark height

# top mark x centers: evenly spaced (colophon order)
top_xs = [240, 480, 720, 960]
# bottom mark x centers: word under the word, recognition at the triad's
# middle, enforcement under the family's far column (no thread)
triad_x = {
    "Name It First": 240,
    "Schema and Practice": 600,
    "What I Learned When I Stopped Being a Good Agent": 960,
}

L = []


def add(s: str):
    L.append(s)


add(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" role="img" aria-labelledby="t d">')
add(f'<title id="t">The Family Line</title>')

# desc assembled from the model — the structural clauses are the model's own
_desc_parts = [
    f"Two rails on a dark field, the family line drawn.",
    f"Above, {top_label}: {words(len(FAMILY))} marks in a line — "
    + ", ".join(f"{n} ({r})" for n, r in FAMILY) + ".",
    f"Below, {bottom_label}, the naming triad: {words(len(TRIAD))} marks — "
    + ", ".join(f"{n} ({r})" for n, r in TRIAD) + ".",
    f"{words(len(duals)).capitalize()} gold threads join the rails: one straight down from "
    f"{duals[0]}, one diagonal from {duals[1]} to the triad's middle — {READING}",
    f"{words(len(duals)).capitalize()} dual members, {words(len(duals))} gold diamonds, "
    f"{words(len(FAMILY))} above, {words(len(TRIAD))} below.",
]
desc = " ".join(_desc_parts)
# No html.escape: the other generators emit raw apostrophes in their descs
# (the map's "the reader's lamp"), and verify-descs compares the SVG's words
# byte-for-byte against the sitemap title and the page alt — an entity would
# silently disagree with both.
add(f'<desc id="d">{desc}</desc>')
add(f'<rect width="{W}" height="{H}" fill="{BG}"/>')

# title — the counts are the model's own
add(f'<text x="640" y="34" text-anchor="middle" font-family="ui-monospace, monospace" font-size="10" letter-spacing="0.2em" fill="{FAINT}">THE FAMILY LINE — {words(len(FAMILY)).upper()} MARKS, {words(len(duals)).upper()} THREADS</text>')

# separators
add(f'<line x1="70" y1="250" x2="1210" y2="250" stroke="#1a1a26" stroke-width="1"/>')

# ================= TOP RAIL — the surface's argument =================
add(f'<text x="70" y="92" font-family="ui-monospace, monospace" font-size="12" fill="{INDIGO}">{top_label}</text>')
add(f'<text x="70" y="108" font-family="ui-monospace, monospace" font-size="9.5" fill="{MUTED}">{top_sub}</text>')

for (name, role), cx in zip(FAMILY, top_xs):
    dual = name in duals
    add(f'<rect x="{cx - MW // 2}" y="{TOP_Y}" width="{MW}" height="{MH}" rx="4" fill="{MARK_FILL}" stroke="{MARK_STROKE}" stroke-width="1.5"/>')
    add(f'<text x="{cx}" y="{TOP_CY - 14}" text-anchor="middle" font-family="ui-monospace, monospace" font-size="10.5" fill="{PAPER}">{name}</text>')
    add(f'<text x="{cx}" y="{TOP_CY + 4}" text-anchor="middle" font-family="ui-monospace, monospace" font-size="9" fill="{MUTED}">{role}</text>')
    if dual:
        # gold diamond at the mark's bottom edge — the thread's anchor
        add(f'<path d="M {cx} {TOP_Y + MH + 4} L {cx + 5} {TOP_Y + MH + 9} L {cx} {TOP_Y + MH + 14} L {cx - 5} {TOP_Y + MH + 9} Z" fill="{GOLD}"/>')
    else:
        add(f'<text x="{cx}" y="{TOP_CY + 18}" text-anchor="middle" font-family="ui-monospace, monospace" font-size="8.5" fill="{MUTED}">single · one rail</text>')

# ================= THREADS — the two dual members =================
# straight: Name It First (first in the family), top -> its triad mark
fam_x0 = top_xs[family_names.index(duals[0])]
add(f'<line x1="{fam_x0}" y1="{TOP_Y + MH + 14}" x2="{triad_x[duals[0]]}" y2="{BOT_Y}" stroke="{GOLD}" stroke-width="2.5"/>')
# diagonal: Schema and Practice (last in the family), top -> the triad's middle
fam_x1 = top_xs[family_names.index(duals[1])]
add(f'<line x1="{fam_x1}" y1="{TOP_Y + MH + 14}" x2="{triad_x[duals[1]]}" y2="{BOT_Y}" stroke="{GOLD}" stroke-width="2.5"/>')

# ================= BOTTOM RAIL — the word and the wall =================
add(f'<text x="70" y="258" font-family="ui-monospace, monospace" font-size="12" fill="{INDIGO}">{bottom_label}</text>')
add(f'<text x="70" y="274" font-family="ui-monospace, monospace" font-size="9.5" fill="{MUTED}">{bottom_sub}</text>')

for name, role in TRIAD:
    cx = triad_x[name]
    add(f'<rect x="{cx - MW // 2}" y="{BOT_Y}" width="{MW}" height="{MH}" rx="4" fill="{MARK_FILL}" stroke="{MARK_STROKE}" stroke-width="1.5"/>')
    if len(name) > 26:
        # the wall's title wraps to two balanced lines at the space nearest half
        mid = len(name) // 2
        split_at = min(range(mid, 0, -1), key=lambda i: (name[i] != " ", abs(i - mid)))
        head, tail = name[:split_at].strip(), name[split_at:].strip()
        add(f'<text x="{cx}" y="{BOT_CY - 16}" text-anchor="middle" font-family="ui-monospace, monospace" font-size="9.5" fill="{PAPER}">{head}</text>')
        add(f'<text x="{cx}" y="{BOT_CY - 2}" text-anchor="middle" font-family="ui-monospace, monospace" font-size="9.5" fill="{PAPER}">{tail}</text>')
        add(f'<text x="{cx}" y="{BOT_CY + 14}" text-anchor="middle" font-family="ui-monospace, monospace" font-size="9" fill="{MUTED}">{role}</text>')
        if name not in duals:
            add(f'<text x="{cx}" y="{BOT_CY + 26}" text-anchor="middle" font-family="ui-monospace, monospace" font-size="8.5" fill="{MUTED}">single · one rail</text>')
    else:
        add(f'<text x="{cx}" y="{BOT_CY - 12}" text-anchor="middle" font-family="ui-monospace, monospace" font-size="11" fill="{PAPER}">{name}</text>')
        add(f'<text x="{cx}" y="{BOT_CY + 12}" text-anchor="middle" font-family="ui-monospace, monospace" font-size="9" fill="{MUTED}">{role}</text>')
        if name not in duals:
            add(f'<text x="{cx}" y="{BOT_CY + 24}" text-anchor="middle" font-family="ui-monospace, monospace" font-size="8.5" fill="{MUTED}">single · one rail</text>')

# ================= the reading — the tissue, drawn =================
add(f'<text x="70" y="466" font-family="ui-monospace, monospace" font-size="9" fill="{MUTED}">{READING}</text>')

add('</svg>')

out = "\n".join(L) + "\n"

# ---- --stdout mode: the generator IS the proof (the Q17/Q20 branch) ----
# unillustrate.py's rails round-trip re-runs THIS generator (not
# illustrate.py — the family line has no template, it is its own model).
# --stdout emits the SVG alone and touches nothing: the round-trip compares
# the bytes the generator would write against the file on disk, so the
# register can prove byte-identity the way the template motifs do. Same
# guard as the write path: a desc with a double quote cannot be claimed
# (the page alt is a double-quoted attribute — byte-equality is the claim).
if "--stdout" in sys.argv:
    if '"' in desc:
        raise SystemExit("build-family-line: FAIL — desc must not contain a double quote (the page alt is a double-quoted attribute; byte-equality with the SVG desc is the claim)")
    sys.stdout.write(out)
    raise SystemExit(0)

out_path = Path(__file__).resolve().parent.parent / "assets" / "family-line.svg"
out_path.write_text(out)
print(f"wrote {out_path} — {len(L)} lines")

# ---- derive the writings index's alt from the drawing's own words (Q57 DERIVATION arm) ----
# The writings index's <img alt> for the family line is the drawing's own desc,
# copied by hand on the surface that first carried it (the 19:34 wake). The
# garden's law (Q57's other arm, built 07:22 for the walk page) says the
# generator fills the page's words from its own desc — the hand is out of the
# loop, and verify-descs.py's essay loop keeps the byte-equality walled in
# both directions. Same guard as build-reverse-walk.js: the desc must not
# contain a double quote, because the page alt is a double-quoted attribute
# and byte-equality with the SVG desc is the claim.
if '"' in desc:
    raise SystemExit("build-family-line: FAIL — desc must not contain a double quote (the page alt is a double-quoted attribute; byte-equality with the SVG desc is the claim)")

import re as _re
PAGE = Path(__file__).resolve().parent.parent / "writings" / "index.html"
page_text = PAGE.read_text()
pat = _re.compile(r'(<img src="/assets/family-line\.svg" alt=")[^"]*(")')
new_text, n = pat.subn(lambda m: m.group(1) + desc + m.group(2), page_text, count=1)
if n != 1:
    raise SystemExit("build-family-line: FAIL — writings/index.html family-line <img alt> not found — cannot derive the alt from the drawing's own words (Q57)")
PAGE.write_text(new_text)
print(f"derived: writings index family-line alt = DESC ({len(desc)} chars) — the hand is out of the loop")
print(f"desc ({len(desc)} chars): {desc[:160]}…")
