#!/usr/bin/env python3
"""Canonical pup hero build (dev-skills/pup-hero-image).

Rebuilds the FULL pup wiki hero from assets: background, ADOM logo, "Pup" title,
sandbox subtitle, solid BRIDGE pill, the vitrine (a real pup Chrome window showing a
wiki page, taskbar band with badged pup tiles, bottom fade), the Windows-11 jump-list
flyout, drop shadow, rounded corners. Output: hero.png at 2000x1250 (16:10).

Usage:
    python3 build_hero.py --content <screenshot.png> [--out hero.png]

--content is the page screenshot shown INSIDE the vitrine: take a FRESH one with
pup_screenshot_full_res of a good-looking wiki page (the shipped hero used the Alex
LED-nameplate hardware page). Do NOT reuse a stale screenshot of an old UI.

Design constants are matched to Bridge's hero (adom/adom-bridge) — measured, not
guessed. If Bridge's hero changes, re-measure before touching these.
"""
import argparse, os
from PIL import Image, ImageDraw, ImageFilter, ImageFont

HERE = os.path.dirname(os.path.abspath(__file__))
A = lambda *p: os.path.join(HERE, 'assets', *p)

ap = argparse.ArgumentParser()
ap.add_argument('--content', required=True, help='page screenshot for the vitrine body')
ap.add_argument('--out', default='hero.png')
args = ap.parse_args()

def load(f): return Image.open(f).convert('RGBA')

W, H = 2000, 1250
CQ = W / 100.0                      # 1cqw = 20px on this canvas

# ── background: near-black with a soft teal glow upper-right ─────────────────
bg = Image.new('RGB', (W, H), (13, 17, 23))
glow = Image.new('L', (W, H), 0)
ImageDraw.Draw(glow).ellipse([W*0.45, -H*0.3, W*1.25, H*0.9], fill=90)
glow = glow.filter(ImageFilter.GaussianBlur(240))
bg = Image.composite(Image.new('RGB', (W, H), (18, 52, 58)), bg, glow).convert('RGBA')

# ── vitrine geometry ─────────────────────────────────────────────────────────
# Right edge ALIGNS with the BRIDGE pill's right edge (both inset 2.6cqw = 52px).
# John 2026-08-09: "vitrine is a bit too far to the right, doesn't line up with the pill".
vy = 140
vw = 1062
vx = (W - int(2.6*CQ)) - vw          # 886: right edge at W-52 = pill right edge
VH = 1050
TB = 68                              # taskbar band height
win_h = VH - TB

# ── window chrome strip (real captured Chrome titlebar) ──────────────────────
chrome = Image.open(A('win_slot3.png')).convert('RGB'); CH_SRC = 125
# the capture carries ~12px of desktop residue on EACH side (purple wallpaper sliver left, grey
# border sliver right) — John spotted it stretched down the chrome band (2026-08-09). Trim it.
EDGE_TRIM = 12
chrome = chrome.crop((EDGE_TRIM, 0, chrome.width-EDGE_TRIM, CH_SRC)).resize((vw, int(CH_SRC*vw/1116)), Image.LANCZOS)
CH = chrome.height

# ── page content, cover-fit, bottom 10% fade ─────────────────────────────────
content = Image.open(args.content).convert('RGB'); CONT_H = win_h - CH
s = max(vw/content.width, CONT_H/content.height)
content = content.resize((int(content.width*s), int(content.height*s)), Image.LANCZOS)
cx = (content.width - vw)//2
content = content.crop((cx, 0, cx+vw, CONT_H))
panel = Image.new('RGBA', (vw, VH), (24, 26, 31, 255))
panel.paste(chrome.convert('RGBA'), (0, 0))
panel.paste(content.convert('RGBA'), (0, CH))
fade_h = int(CONT_H*0.10)
alpha = Image.linear_gradient('L').resize((vw, fade_h))
solid = Image.new('RGBA', (vw, fade_h), (24, 26, 31, 255)); solid.putalpha(alpha)
panel.alpha_composite(solid, (0, CH+CONT_H-fade_h))

# ── taskbar band: 5 badged pup tiles, LEFT-aligned, no start orb ─────────────
dp = ImageDraw.Draw(panel); tb_y = VH - TB
dp.rectangle([0, tb_y, vw, VH], fill=(24, 26, 31, 255))
dp.line([0, tb_y, vw, tb_y], fill=(44, 50, 60, 255))
tiles = [load(A('tiles', f'pup-cat-{k}9.ico')) for k in ('default', 'mixed', 'app', 'default', 'mixed')]
badges = ['sample-shotlog', 'domains-wiki', 'sample-charger', 'pup-hero', 'gwsrc-draft']
def button(base, bp, size=110):
    b = base.resize((size, size), Image.LANCZOS).copy()
    ov = load(A('badges', f'{bp}.png')).resize((int(size*0.52),)*2, Image.LANCZOS)
    b.alpha_composite(ov, (size-ov.width-2, size-ov.height-2)); return b
bh, gap, x = 54, 14, 14
icon_lefts = []
for i in range(5):
    bt = button(tiles[i], badges[i]).resize((bh, bh), Image.LANCZOS)
    by = tb_y + (TB-bh)//2
    panel.alpha_composite(bt, (x, by)); icon_lefts.append(x)
    if i == 0: dp.line([x+5, VH-4, x+bh-5, VH-4], fill=(120, 200, 255), width=4)  # active underline
    x += bh + gap

# ── jump-list flyout above the ACTIVE (first) icon ───────────────────────────
jl = ImageDraw.Draw(panel)
f_item = ImageFont.truetype(A('satoshi-500.ttf'), 23)
f_hdr  = ImageFont.truetype(A('satoshi-500.ttf'), 15)
recent = ["Pup - Puppeteer Bridge", "Adom Bridge", "Alex - Adom Molecule LED Nameplate"]
tasks  = ["New pup window"]
pw_, pad, row_h, top_pad, hdr_h, div_gap, bot_pad, r = 366, 16, 46, 12, 30, 22, 12, 12
ph_ = top_pad + hdr_h + row_h*len(recent) + div_gap + hdr_h + row_h*len(tasks) + bot_pad
pl = max(6, icon_lefts[0] - 2)                    # panel-local x
pb = tb_y - 10; pt = pb - ph_
# shadow
psh = Image.new('RGBA', panel.size, (0, 0, 0, 0))
ImageDraw.Draw(psh).rounded_rectangle([pl, pt, pl+pw_, pb], radius=r, fill=(0, 0, 0, 150))
panel.alpha_composite(psh.filter(ImageFilter.GaussianBlur(18)))
jl.rounded_rectangle([pl, pt, pl+pw_, pb], radius=r, fill=(41, 41, 44, 252), outline=(78, 78, 84, 255), width=1)
def favicon(cx_, cy_, s_=26):
    x0, y0 = cx_-s_//2, cy_-s_//2
    jl.rounded_rectangle([x0, y0, x0+s_, y0+s_], radius=6, fill=(0, 206, 194, 255))
    gr = s_//2-6
    jl.ellipse([cx_-gr, cy_-gr, cx_+gr, cy_+gr], outline=(9, 26, 28, 255), width=2)
    jl.line([cx_-gr, cy_, cx_+gr, cy_], fill=(9, 26, 28, 255), width=1)
    jl.line([cx_, cy_-gr, cx_, cy_+gr], fill=(9, 26, 28, 255), width=1)
def trunc(t, f, mw):
    if jl.textlength(t, font=f) <= mw: return t
    while t and jl.textlength(t+"…", font=f) > mw: t = t[:-1]
    return t+"…"
y = pt + top_pad
def section(t):
    global y; jl.text((pl+pad, y+6), t.upper(), font=f_hdr, fill=(150, 150, 156, 255)); y += hdr_h
def item(t, hl=False):
    global y
    if hl: jl.rounded_rectangle([pl+6, y+3, pl+pw_-6, y+row_h-3], radius=7, fill=(58, 58, 64, 255))
    favicon(pl+pad+13, y+row_h//2)
    tx = pl+pad+40
    jl.text((tx, y+row_h//2-13), trunc(t, f_item, pl+pw_-pad-tx), font=f_item, fill=(236, 236, 239, 255))
    y += row_h
section("Recent")
for i, t in enumerate(recent): item(t, hl=(i == 0))
y += div_gap//2; jl.line([pl+pad, y, pl+pw_-pad, y], fill=(70, 70, 76, 255), width=1); y += div_gap//2
section("Tasks")
for t in tasks: item(t)

# ── composite vitrine: rounded corners + drop shadow ─────────────────────────
R = 22
mask = Image.new('L', (vw, VH), 0); ImageDraw.Draw(mask).rounded_rectangle([0, 0, vw, VH], R, fill=255)
sh = Image.new('RGBA', (W, H), (0, 0, 0, 0)); shm = Image.new('L', (W, H), 0)
ImageDraw.Draw(shm).rounded_rectangle([vx+10, vy+16, vx+vw+10, vy+VH+16], R, fill=150)
shm = shm.filter(ImageFilter.GaussianBlur(30)); sh.putalpha(shm)
bg = Image.alpha_composite(bg, sh); bg.paste(panel, (vx, vy), mask)

def measured_offset(text, font):
    """Empirical ink offset: PIL's textbbox under-reports the left bearing for Familjen at 240px
    (kicad-thread catch 2026-08-09: the title sat 18px right of the logo). Draw on scratch and
    measure where ink ACTUALLY lands, then compensate. Never trust textbbox for alignment."""
    sc = Image.new('RGB', (1400, 500), (0, 0, 0))
    ImageDraw.Draw(sc).text((100, 100), text, font=font, fill=(255, 255, 255))
    bb = sc.getbbox()
    return bb[0] - 100, bb[1] - 100

# ── left content: logo, title, subtitle (matched to Bridge's hero) ───────────
d = ImageDraw.Draw(bg); inset = int(4.5*CQ)
logo = Image.open(A('adom_logo_white.png')).convert('RGBA'); lw = int(11.8*CQ)
logo = logo.resize((lw, int(logo.height*lw/logo.width)), Image.LANCZOS)
_la = logo.getbbox()   # the asset carries transparent padding; land the INK at the inset
bg.alpha_composite(logo, (inset - _la[0], int(3.2*CQ) - _la[1]))
f_title = ImageFont.truetype(A('familjen-700.ttf'), int(12*CQ))         # 12cqw = 240px em
tox, toy = measured_offset("Pup", f_title); ty = int(13.15*CQ)
d.text((inset-tox, ty-toy), "Pup", font=f_title, fill=(230, 237, 243, 255))
bbt = d.textbbox((0, 0), "Pup", font=f_title)   # still used for subtitle y-flow
f_sub = ImageFont.truetype(A('satoshi-500.ttf'), 50)
BLU, WHT = (74, 168, 255, 255), (232, 238, 244, 255)
sy = ty + (bbt[3]-bbt[1]) + int(2.6*CQ)
# left-bearing compensation on EVERY text draw so all ink edges align with the logo's ink edge
# (kicad-thread catch 2026-08-09: the title compensated, the subtitle didn't -> ragged left edge)
sox, _ = measured_offset("A real browser ", f_sub)
d.text((inset - sox, sy), "A real browser ", font=f_sub, fill=WHT)
w1 = d.textlength("A real browser ", font=f_sub)
d.text((inset - sox + w1, sy), "sandbox", font=f_sub, fill=BLU)
sox2, _ = measured_offset("you fully control.", f_sub)
d.text((inset - sox2, sy + int(50*1.35)), "you fully control.", font=f_sub, fill=WHT)

# ── BRIDGE pill: solid teal, top-right, inset 2.6cqw ─────────────────────────
pf = ImageFont.truetype(A('familjen-700.ttf'), 26); txt = "BRIDGE"
tb2 = d.textbbox((0, 0), txt, font=pf); tw, th = tb2[2]-tb2[0], tb2[3]-tb2[1]
padx, pady = 22, 12; pw2, ph2 = tw+padx*2, th+pady*2
px_ = W - int(2.6*CQ) - pw2; py_ = int(2.6*CQ)
d.rounded_rectangle([px_, py_, px_+pw2, py_+ph2], radius=ph2//2, fill=(0, 206, 194, 255))
d.text((px_+padx-tb2[0], py_+pady-tb2[1]), txt, font=pf, fill=(9, 20, 24, 255))

bg.convert('RGB').save(args.out)
print(f"saved {args.out}  vitrine right edge={vx+vw}  pill right edge={W-int(2.6*CQ)}  (aligned)")
