#!/usr/bin/env -S uv run --with fonttools --with brotli --with uharfbuzz --quiet python
"""Politick "Broadcast · Soft red" — the HERO KEY-VISUAL (clean true-vector composition).

Rebuilds the decided AI render `design/brand/broadcast-softred/key-visual.png` as a clean,
self-contained, font-INDEPENDENT SVG composed from the REAL Wave-1 vector assets — never a
re-trace of the raster, never live-text-with-font-dependency.

WHAT IT COMPOSES (all reused from the locked generators, nothing re-drawn)
--------------------------------------------------------------------------
  - the MARK            -> `logo/mark.py` (via `wordmark.mark_geometry`)
  - the "Politick"      -> `logo/wordmark.py` outlined Playfair 600 paths
  - the tagline block   -> `logo/wordmark.py` `tagline_block` (red rule + tracked caps,
                            "ON RECORD." in soft red)
  - the body paragraph  -> outlined IBM Plex Sans 400 (3 lines), muted ink
  - the meta line       -> outlined IBM Plex Mono 400 words, separated by soft-red MOTIF DOTS
                            (the AI render set this line in GREEN — OFF-BRAND; we use
                            ink words + soft-red dot separators instead)

Everything is OUTLINED to true `<path>`/`<circle>` geometry so the hero renders pixel-
identically in cairosvg, Chromium and print with NO font dependency. Coordinate space is the
wordmark's font units (upem=1000); the mark is scaled into it. Soft red appears ONLY on the
mark's tittle dot, "ON RECORD.", the tagline rule and the meta-line dot separators — every
other element is ink. No green. No rounded corners. No shadows.

THEMEABLE: ink = currentColor (root `style="color:…"`); the accent + muted greys are CSS
custom properties set on the root (`--pk-accent`, `--pk-muted`) with literal fallbacks, so the
same generator emits a paper (`key-visual.svg`) and an ink (`key-visual-ink.svg`) variant.

Re-runnable:  uv run keyvisual.py
"""
from __future__ import annotations

import io
import sys
from dataclasses import dataclass
from pathlib import Path

from fontTools.misc.transform import Transform
from fontTools.pens.boundsPen import BoundsPen
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.transformPen import TransformPen
from fontTools.ttLib import TTFont

OUT = Path(__file__).parent
LOGO = OUT.parent / "logo"
FONTS = OUT.parent / "tokens" / "fonts"

sys.path.insert(0, str(LOGO))
import wordmark as wm          # the LOCKED logotype generator (do NOT modify it)

SANS_WOFF2 = FONTS / "plexsans-400-normal-latin.woff2"   # IBM Plex Sans Regular (body)
MONO_WOFF2 = FONTS / "plexmono-400-normal-latin.woff2"   # IBM Plex Mono Regular (meta line)

f = wm.f                       # compact float formatter (house style)
ACCENT_VAR = "var(--pk-accent, #B84A38)"

# --------------------------------------------------------------------------- copy
BODY_LINES = [
    "A non-partisan public-interest platform that makes",
    "a country's parliamentary record readable,",
    "searchable and verifiable.",
]
META_SEGMENTS = ["read", "search", "verify", "made public"]   # soft-red dots between them


# --------------------------------------------------------------------------- theme
@dataclass(frozen=True)
class Theme:
    name: str
    paper: str       # background surface
    ink: str         # currentColor (text + strokes)
    accent: str      # --pk-accent value
    muted: str       # --pk-muted value (body + meta words)


PAPER_THEME = Theme("paper", paper="#F6F3EC", ink="#1C2024", accent="#B84A38", muted="#5C564E")
INK_THEME = Theme("ink", paper="#1C2024", ink="#F6F3EC", accent="#CF6A55", muted="#A6A39C")

MUTED_VAR = "var(--pk-muted, #5C564E)"


# --------------------------------------------------------------------------- params
@dataclass(frozen=True)
class Params:
    """Single source of truth. The base unit is the wordmark cap/ascender height WH (read from
    the outlined "Politick"); every dimension below is a pure RATIO of WH — no magic pixels."""
    # --- mark (left), scaled into the wordmark's font-unit space ---
    mark_h: float = 2.62        # mark height / WH
    mark_gap: float = 0.62      # gap between mark right edge and the text column / WH

    # --- vertical rhythm of the right-hand text column (all / WH unless noted) ---
    tag_gap: float = 0.46       # wordmark baseline -> tagline cap-top
    body_gap: float = 0.72      # tagline baseline -> first body baseline
    body_fill: float = 0.99     # longest body line width / WW (auto-fits body type size)
    body_lead: float = 1.52     # body line pitch (× body_em)
    meta_gap: float = 0.72      # last body baseline -> meta baseline
    meta_em: float = 0.215      # meta (mono) type size (em) / WH
    meta_track: float = 0.01    # meta letter-spacing (em)

    # --- meta line dot separators (the soft-red motif dots) ---
    meta_word_gap: float = 0.62  # ink-gap between words / meta_em
    meta_dot_r: float = 0.072    # separator dot radius / meta_em
    meta_dot_y: float = 0.30     # dot centre above baseline / meta_em (optical x-height centre)

    # --- canvas ---
    pad_x: float = 0.95         # left/right artboard padding / WH
    pad_y: float = 0.92         # top/bottom artboard padding / WH


P = Params()


# --------------------------------------------------------------------------- outlined run
def line_run(woff2: Path, weight: int, text: str, em: float, left: float, baseline: float,
             tracking_em: float = 0.0, align_ink_left: bool = True) -> dict:
    """Outline `text` at type size `em` (in font-unit space), baseline at `baseline`, ink left
    edge at `left` (or pen origin at `left` if align_ink_left=False). Returns the `<path>` data
    plus measured ink box. Real HarfBuzz shaping + fontTools outlines — never raster/sampled."""
    raw = wm.load_static(woff2, weight)
    placed = wm.shape(raw, text, tracking_em)
    tt = TTFont(io.BytesIO(raw))
    gs = tt.getGlyphSet()
    upem = tt["head"].unitsPerEm
    g = em / upem                                   # font units -> design units at this size

    bp = BoundsPen(gs)
    for gl in placed:
        gs[gl["name"]].draw(TransformPen(bp, Transform().translate(gl["px"], gl["py"])))
    if bp.bounds is None:
        raise RuntimeError(f"empty outline bounds for {text!r}")
    xmin, ymin, xmax, ymax = bp.bounds

    x0 = left - (xmin * g if align_ink_left else 0.0)   # place ink left edge at `left`
    tr = Transform().translate(x0, baseline).scale(g, -g)
    pen = SVGPathPen(gs)
    for gl in placed:
        gs[gl["name"]].draw(TransformPen(pen, tr.translate(gl["px"], gl["py"])))
    return dict(d=pen.getCommands(),
                ink_left=x0 + xmin * g, ink_right=x0 + xmax * g,
                cap=ymax * g, desc=-ymin * g)


# --------------------------------------------------------------------------- build
def build() -> dict:
    """Compose the whole hero in the wordmark's font-unit space. Returns the body markup
    (theme-neutral: currentColor + var(--pk-accent) + var(--pk-muted)) and the tight viewBox."""
    # --- wordmark (outlined Playfair 600) — the base unit WH comes from here ---
    recs, vb = wm.build_wordmark_recs()
    WW, WH = vb[2], vb[3]

    # text column origin: leave room for the mark on the left
    markup_mark, mbox = wm.mark_geometry()
    mh = mbox[3]
    s_mark = (P.mark_h * WH) / mh
    MW = s_mark * mbox[2]
    TX = P.mark_gap * WH + MW                       # text column left edge

    body: list[str] = []

    # --- "Politick" wordmark (top-left of the column), baseline at WH ---
    body.append(wm.placed_wordmark(recs, TX, 0.0))
    word_baseline = WH

    # --- tagline block (red rule + tracked caps, "ON RECORD." in accent), spanning WW ---
    tag_top = word_baseline + P.tag_gap * WH
    tag_svg, tag_cap, tag_bot = wm.tagline_block(TX, tag_top, WW)
    body.append(tag_svg)

    # --- body paragraph: 3 outlined IBM Plex Sans 400 lines, muted ink ---
    # auto-fit the type size so the LONGEST line spans body_fill·WW (matches the reference,
    # where the body block sits flush under the wordmark) — no magic pixel size.
    probe = [line_run(SANS_WOFF2, 400, ln, 1000.0, TX, 0.0) for ln in BODY_LINES]
    max_w = max(r["ink_right"] - TX for r in probe)
    body_em = P.body_fill * WW / max_w * 1000.0
    body_first = tag_bot + P.body_gap * WH
    body_paths: list[str] = []
    body_right = TX
    by = body_first
    for line in BODY_LINES:
        r = line_run(SANS_WOFF2, 400, line, body_em, TX, by)
        body_paths.append(r["d"])
        body_right = max(body_right, r["ink_right"])
        last_body_baseline = by
        body_desc = r["desc"]
        by += P.body_lead * body_em
    body.append(f'<g fill="{MUTED_VAR}">'
                + "".join(f'<path d="{d}"/>' for d in body_paths) + "</g>")
    body_block_bot = last_body_baseline + body_desc

    # --- meta line: outlined IBM Plex Mono words + soft-red MOTIF DOTS as separators ---
    meta_em = P.meta_em * WH
    meta_baseline = last_body_baseline + P.meta_gap * WH
    dot_r = P.meta_dot_r * meta_em
    dot_cy = meta_baseline - P.meta_dot_y * meta_em
    word_gap = P.meta_word_gap * meta_em

    meta_word_paths: list[str] = []
    meta_dots: list[str] = []
    x = TX
    for i, seg in enumerate(META_SEGMENTS):
        r = line_run(MONO_WOFF2, 400, seg, meta_em, x, meta_baseline,
                     tracking_em=P.meta_track, align_ink_left=True)
        meta_word_paths.append(r["d"])
        x = r["ink_right"]
        if i < len(META_SEGMENTS) - 1:
            dot_cx = x + word_gap / 2
            meta_dots.append(
                f'<circle cx="{f(dot_cx)}" cy="{f(dot_cy)}" r="{f(dot_r)}" fill="{ACCENT_VAR}"/>')
            x = x + word_gap
    meta_right = x
    body.append(f'<g fill="currentColor">'
                + "".join(f'<path d="{d}"/>' for d in meta_word_paths) + "</g>")
    body.append("".join(meta_dots))

    # --- mark (left), vertically centred on the wordmark→body block (meta sits below) ---
    MH = P.mark_h * WH
    mark_cy = (0.0 + body_block_bot) / 2
    mark_x = 0.0
    mark_y = mark_cy - MH / 2
    body.insert(0, wm.placed_mark(markup_mark, mbox, mark_x, mark_y, s_mark))

    # --- tight content bounds -> viewBox with padding ---
    content_left = mark_x
    content_right = max(TX + WW, body_right, meta_right)
    content_top = min(mark_y, 0.0)
    content_bot = max(mark_y + MH, meta_baseline + P.meta_dot_y * meta_em)
    pad_x = P.pad_x * WH
    pad_y = P.pad_y * WH
    vbx = (content_left - pad_x, content_top - pad_y,
           (content_right - content_left) + 2 * pad_x,
           (content_bot - content_top) + 2 * pad_y)
    return dict(body="".join(body), viewBox=vbx, WH=WH, WW=WW, MW=MW, MH=MH,
                meta_right=meta_right, tag_bot=tag_bot)


# --------------------------------------------------------------------------- emit
def emit(theme: Theme, b: dict) -> str:
    vb = b["viewBox"]
    style = (f"color:{theme.ink};--pk-accent:{theme.accent};--pk-muted:{theme.muted}")
    return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="{f(vb[0])} {f(vb[1])} {f(vb[2])} {f(vb[3])}"
     role="img" aria-label="Politick — Parliament. On record. For every citizen." style="{style}">
  <rect x="{f(vb[0])}" y="{f(vb[1])}" width="{f(vb[2])}" height="{f(vb[3])}" fill="{theme.paper}"/>
  {b["body"]}
</svg>
'''


EXPORT_W = 2520   # px width of the high-res Chromium export for the bible (>= 2400)


def emit_export_html(svg: str, width: int) -> str:
    """A minimal page that sizes the (fully self-contained) SVG to `width` px so a Chromium
    full-page screenshot yields the truthful high-res hero PNG. Margin 0; the SVG carries its
    own paper background, so the screenshot needs no page styling."""
    return f'''<!doctype html><meta charset="utf-8"><title>Politick key-visual export</title>
<style>html,body{{margin:0;padding:0}} svg{{display:block;width:{width}px;height:auto}}</style>
{svg}'''


def main():
    b = build()
    paper_svg = emit(PAPER_THEME, b)
    (OUT / "key-visual.svg").write_text(paper_svg)
    (OUT / "key-visual-ink.svg").write_text(emit(INK_THEME, b))
    (OUT / "export.html").write_text(emit_export_html(paper_svg, EXPORT_W))
    vb = b["viewBox"]
    print(f"wrote key-visual.svg + key-visual-ink.svg + export.html (export {EXPORT_W}px)")
    print(f"  viewBox = {f(vb[0])} {f(vb[1])} {f(vb[2])} {f(vb[3])}  (aspect {vb[2]/vb[3]:.3f})")
    print(f"  WH(base)={f(b['WH'])}  WW={f(b['WW'])}  mark={f(b['MW'])}x{f(b['MH'])}")


if __name__ == "__main__":
    main()
