#!/usr/bin/env python3
"""Regenerate the landing site's release content from docs/releases.json.

Owns two things:
  1. The "New in X" section of docs/index.html (between WN:BEGIN/WN:END
     markers), rendered from the newest entry — plus the hero version string.
  2. docs/releases.html, the full release history page, rendered whole.

Usage:
  python3 scripts/site-release.py               # regenerate both
  python3 scripts/site-release.py --check       # exit 1 if output would change (CI)
  python3 scripts/site-release.py --list-icons  # show valid icon + color names

Stdlib only. Keep it that way — this runs in the Pages deploy job.
"""

import argparse
import json
import re
import sys
from datetime import date
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
DOCS = ROOT / "docs"
DATA = DOCS / "releases.json"
INDEX = DOCS / "index.html"
HISTORY = DOCS / "releases.html"

WN_BEGIN = "<!-- WN:BEGIN generated by scripts/site-release.py — edit docs/releases.json instead -->"
WN_END = "<!-- WN:END -->"

REPO = "https://github.com/caezium/Burrow"

# Jewel tokens that exist as CSS variables in the site stylesheet.
COLORS = {"teal", "coral", "violet", "azure", "gold", "moss", "ginger",
          "green", "red", "lilac", "apricot", "mint", "accent"}

# Small stroke-icon library (24×24, stroke = currentColor handled by CSS).
ICONS = {
    "home":        '<path d="M4 11l8-7 8 7"/><path d="M6 9.5V20h12V9.5"/><path d="M10 20v-5h4v5"/>',
    "globe":       '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.6 2.5 4 5.6 4 9s-1.4 6.5-4 9c-2.6-2.5-4-5.6-4-9s1.4-6.5 4-9z"/>',
    "thermo":      '<path d="M14 14.8V5a2 2 0 10-4 0v9.8a4 4 0 104 0z"/><path d="M12 11v6"/>',
    "pulse":       '<path d="M3 12h4l2.5-7 4 14 2.5-7h5"/>',
    "trash":       '<path d="M4 7h16M9 7V5h6v2M6 7l1 13h10l1-13"/><path d="M10 11v5M14 11v5"/>',
    "sparkle":     '<path d="M12 4l1.8 4.6L18 10l-4.2 1.4L12 16l-1.8-4.6L6 10l4.2-1.4z"/><path d="M18.5 15.5l.9 2.1 2.1.9-2.1.9-.9 2.1-.9-2.1-2.1-.9 2.1-.9z"/>',
    "wrench":      '<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/>',
    "flask":       '<path d="M10 3v6L4.5 18a2 2 0 001.8 3h11.4a2 2 0 001.8-3L14 9V3"/><path d="M8.5 3h7M7 15h10"/>',
    "package":     '<path d="M21 8l-9-5-9 5v8l9 5 9-5z"/><path d="M3 8l9 5 9-5M12 13v8"/>',
    "gauge":       '<path d="M5 19a9 9 0 1114 0"/><path d="M12 13l4-4"/><circle cx="12" cy="13" r="1.6"/>',
    "chart":       '<path d="M4 20v-6M9 20V8M14 20v-9M19 20V5"/>',
    "list":        '<path d="M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01"/>',
    "sweep":       '<path d="M9.59 4.59A2 2 0 1111 8H2m10.59 11.41A2 2 0 1014 16H2m15.73-8.27A2.5 2.5 0 1119.5 12H2"/>',
    "robot":       '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9l3 3-3 3M13 15h4"/>',
    "shield":      '<path d="M12 3l7 3v5c0 4.5-3 8-7 10-4-2-7-5.5-7-10V6z"/><path d="M9.5 11.5l2 2 3.5-3.5"/>',
    "leaf":        '<path d="M5 19C5 9 13 4 20 4c0 7-3 15-13 15"/><path d="M5 19c2-5 6-9 11-11"/>',
    "fingerprint": '<path d="M4.5 9.5A8 8 0 0120 12"/><path d="M7 12a5 5 0 0110 0c0 3-1 6-2.5 8"/><path d="M12 12c0 3-.6 5.5-1.8 7.6"/>',
    "monitor":     '<rect x="3" y="4" width="18" height="13" rx="2"/><path d="M3 8h18M9 21h6"/>',
    "clock":       '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
    "layout":      '<rect x="3" y="3" width="8" height="11" rx="1"/><rect x="13" y="3" width="8" height="6" rx="1"/><rect x="13" y="11" width="8" height="10" rx="1"/><rect x="3" y="16" width="8" height="5" rx="1"/>',
    "key":         '<circle cx="8" cy="15" r="4"/><path d="M10.8 12.2L20 3l1 1-2 2M16 8l2 2"/>',
}

CHECK_PATH = '<path d="M5 13l4 4L19 7"/>'

MONTHS = ["January", "February", "March", "April", "May", "June", "July",
          "August", "September", "October", "November", "December"]


def esc(s):
    return (s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))


def md_links(s):
    """Escape HTML, then convert [text](url) to anchors."""
    out = esc(s)
    return re.sub(r"\[([^\]]+)\]\(([^)]+)\)",
                  r'<a href="\2" target="_blank" rel="noopener">\1</a>', out)


def month_year(iso):
    y, m, _ = (int(p) for p in iso.split("-"))
    return f"{MONTHS[m - 1]} {y}"


def pretty_date(iso):
    y, m, d = (int(p) for p in iso.split("-"))
    return f"{MONTHS[m - 1][:3]} {d}, {y}"


def icon_svg(name):
    return ('<svg viewBox="0 0 24 24" stroke-linecap="round" '
            f'stroke-linejoin="round">{ICONS[name]}</svg>')


def validate(rel):
    for h in rel["highlights"]:
        if h["icon"] not in ICONS:
            sys.exit(f"releases.json: unknown icon '{h['icon']}' in {rel['version']} "
                     f"(valid: {', '.join(sorted(ICONS))})")
        if h["color"] not in COLORS:
            sys.exit(f"releases.json: unknown color '{h['color']}' in {rel['version']} "
                     f"(valid: {', '.join(sorted(COLORS))})")
    date.fromisoformat(rel["date"])  # raises on bad dates


def render_card(h):
    return (f'      <article class="tcard" style="--c: var(--{h["color"]})">\n'
            f'        <div class="top"><div class="ico">{icon_svg(h["icon"])}</div>'
            f'<div class="nm">{esc(h["title"])}</div></div>\n'
            f'        <p class="ds">{md_links(h["line"])}</p>\n'
            f'      </article>')


def render_chip(text, indent="      "):
    return (f'{indent}<span class="wn-chip"><svg viewBox="0 0 24 24" '
            f'stroke-linecap="round" stroke-linejoin="round">{CHECK_PATH}</svg>'
            f'{md_links(text)}</span>')


def render_whatsnew(rel):
    cards = "\n".join(render_card(h) for h in rel["highlights"])
    chips = "\n".join(render_chip(c) for c in rel["chips"])
    return f"""{WN_BEGIN}
    <div class="sec-head wn-head">
      <div>
        <span class="sec-num">What's new · {month_year(rel["date"])}</span>
        <h2 class="sec-title">New in {esc(rel["version"])}</h2>
      </div>
      <a class="btn btn-out btn-sm" href="releases.html">All releases</a>
    </div>

    <div class="grid3 wn-grid">
{cards}
    </div>

    <div class="extra-label mono">// also tightened in {esc(rel["version"])}</div>
    <div class="wn-chips">
{chips}
    </div>
    {WN_END}"""


def render_history_entry(rel, latest):
    hl = "\n".join(
        f'        <div class="hl" style="--c: var(--{h["color"]})">\n'
        f'          <div class="ico">{icon_svg(h["icon"])}</div>\n'
        f'          <div><b>{esc(h["title"])}</b><p>{md_links(h["line"])}</p></div>\n'
        f'        </div>' for h in rel["highlights"])
    chips = "\n".join(render_chip(c, indent="        ") for c in rel["chips"])
    latest_chip = '<span class="latest">latest</span>' if latest else ""
    return f"""    <article class="rel card" id="{esc(rel["tag"])}">
      <header class="rel-head">
        <h2 class="ver">{esc(rel["version"])}{latest_chip}</h2>
        <span class="date mono">{pretty_date(rel["date"])}</span>
        <a class="gh mono" href="{REPO}/releases/tag/{esc(rel["tag"])}" target="_blank" rel="noopener">full notes ↗</a>
      </header>
      <p class="tagline">{md_links(rel["tagline"])}</p>
      <div class="hls">
{hl}
      </div>
      <div class="wn-chips">
{chips}
      </div>
    </article>"""


def render_history(releases):
    entries = "\n\n".join(render_history_entry(r, i == 0)
                          for i, r in enumerate(releases))
    latest = releases[0]
    head = """<!DOCTYPE html>
<!-- GENERATED by scripts/site-release.py from docs/releases.json — do not edit by hand. -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Burrow · Releases</title>
<meta name="description" content="Every Burrow release at a glance — what each version added, fixed, and tightened.">
<meta name="theme-color" content="#121217">
<link rel="icon" type="image/png" href="burrow-icon.png">
<meta property="og:type" content="website">
<meta property="og:title" content="Burrow · Releases">
<meta property="og:description" content="Every Burrow release at a glance.">
<meta property="og:image" content="https://burrow.henryzh.dev/burrow-icon-512.png">
<meta property="og:url" content="https://burrow.henryzh.dev/releases.html">
<style>
  @font-face { font-family: "Geist"; src: url("assets/fonts/Geist[wght].woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; }
  @font-face { font-family: "Geist Mono"; src: url("assets/fonts/GeistMono[wght].woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; }
  @font-face { font-family: "Cal Sans"; src: url("assets/fonts/CalSans-SemiBold.woff2") format("woff2"); font-weight: 600; font-style: normal; font-display: swap; }
  :root {
    --bg:#121217; --surface:rgba(243,236,221,0.045); --surface-2:rgba(243,236,221,0.07);
    --ink:#F3ECDD; --ink-2:rgba(243,236,221,0.72); --ink-3:rgba(243,236,221,0.46);
    --hair:rgba(243,236,221,0.14); --hair-2:rgba(243,236,221,0.08);
    --accent:#D9A066; --on-accent:#241B12; --lilac:#D6C6FF; --apricot:#FFD3B6; --mint:#C9F2E6;
    --teal:#35C2A5; --coral:#F0714E; --violet:#8E84F0; --azure:#4FA3E3;
    --gold:#E6A93C; --moss:#6FB06A; --ginger:#D98C5F; --green:#57D58E; --red:#F0604E;
    --serif:"Geist",-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
    --mono:"Geist Mono",ui-monospace,"SF Mono",Menlo,monospace;
    --heading:"Cal Sans","Geist",-apple-system,system-ui,sans-serif;
    --r-card:28px; --page:880px; --pad:40px;
  }
  * { box-sizing: border-box; }
  html { -webkit-text-size-adjust: 100%; }
  body { margin:0; color:var(--ink); font-family:var(--serif); font-size:17px; line-height:1.6;
    -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility; overflow-x:hidden;
    background-color:var(--bg);
    background-image:radial-gradient(1200px 600px at 82% -10%, rgba(217,160,102,0.10), transparent 60%);
    background-repeat:no-repeat; background-attachment:fixed; }
  a { color: inherit; text-decoration: none; }
  ::selection { background: rgba(217,160,102,0.26); }
  .page { max-width: var(--page); margin: 0 auto; padding: 0 var(--pad); }
  .mono { font-family: var(--mono); }
  .topbar { position:sticky; top:0; z-index:50; padding:14px 0; background:rgba(18,18,23,0.72);
    backdrop-filter:saturate(150%) blur(14px); -webkit-backdrop-filter:saturate(150%) blur(14px); border-bottom:1px solid var(--hair-2); }
  .topbar .in { display:flex; align-items:center; gap:16px; }
  .brand { display:flex; align-items:center; gap:10px; }
  .brand .mk { width:26px; height:26px; border-radius:8px; }
  .brand .nm { font-family:var(--mono); font-size:15px; }
  .back { margin-left:auto; font-family:var(--mono); font-size:12.5px; color:var(--ink-2); padding:9px 15px;
    border:1px solid var(--hair); border-radius:12px; transition:.15s; }
  .back:hover { color:var(--ink); background:var(--surface-2); }
  .btn { font-weight:600; font-size:13.5px; display:inline-flex; align-items:center; padding:9px 18px;
    border-radius:999px; border:1.5px solid var(--accent); background:var(--accent); color:var(--on-accent); white-space:nowrap; }
  .btn:hover { background:#E3AD77; border-color:#E3AD77; }
  .hero { padding-top: 56px; padding-bottom: 8px; }
  .hero h1 { font-family:var(--heading); font-weight:600; font-size:clamp(40px,7vw,64px); line-height:1.04; margin:0; }
  .hero p { color:var(--ink-2); margin:14px 0 0; max-width:560px; }
  .hero .count { font-family:var(--mono); font-size:12.5px; color:var(--ink-3); margin-top:10px; }
  .card { background:var(--surface); border:1px solid var(--hair-2); border-radius:var(--r-card); box-shadow:0 1px 0 rgba(255,255,255,0.05) inset; }
  main { padding-top: 28px; padding-bottom: 24px; display:grid; gap:18px; }
  .rel { padding: 26px 28px; }
  .rel-head { display:flex; align-items:baseline; gap:14px; flex-wrap:wrap; }
  .ver { font-family:var(--heading); font-weight:600; font-size:30px; margin:0; display:inline-flex; align-items:center; gap:10px; }
  .latest { font-family:var(--mono); font-size:10.5px; letter-spacing:.04em; padding:4px 11px; border-radius:999px;
    background:rgba(217,160,102,0.16); color:var(--accent); border:1px solid rgba(217,160,102,0.3); transform:translateY(-2px); }
  .date { font-size:12.5px; color:var(--ink-3); }
  .gh { margin-left:auto; font-size:12.5px; color:var(--ink-2); }
  .gh:hover { color:var(--accent); }
  .tagline { margin:10px 0 0; font-size:15.5px; color:var(--ink-2); }
  .hls { margin-top:18px; display:grid; grid-template-columns:1fr 1fr; gap:14px 22px; }
  .hl { display:flex; gap:12px; align-items:flex-start; }
  .hl .ico { width:32px; height:32px; border-radius:10px; display:grid; place-items:center; flex:0 0 auto; margin-top:2px;
    background:color-mix(in srgb, var(--c) 16%, transparent); border:1px solid color-mix(in srgb, var(--c) 36%, transparent); }
  .hl .ico svg { width:17px; height:17px; stroke:var(--c); fill:none; stroke-width:1.8; }
  .hl b { font-size:15px; }
  .hl p { margin:3px 0 0; font-size:13.5px; line-height:1.5; color:var(--ink-2); }
  .hl p a { color:var(--accent); text-decoration:underline; text-underline-offset:2px; }
  .wn-chips { margin-top:18px; display:flex; flex-wrap:wrap; gap:9px; }
  .wn-chip { display:inline-flex; align-items:center; gap:8px; font-family:var(--mono); font-size:12px; color:var(--ink-2);
    border:1px solid var(--hair-2); background:var(--surface); border-radius:999px; padding:7px 13px; }
  .wn-chip svg { width:12px; height:12px; stroke:var(--green); fill:none; stroke-width:2.4; flex:0 0 auto; }
  .wn-chip a { color:var(--accent); text-decoration:underline; text-underline-offset:2px; }
  footer { padding:36px 0 56px; }
  footer .page { font-size:13px; color:var(--ink-3); display:flex; gap:14px; flex-wrap:wrap; }
  footer a { color:var(--ink-2); text-decoration:underline; text-underline-offset:2px; }
  footer a:hover { color:var(--accent); }
  @media (max-width: 720px) {
    :root { --pad: 22px; }
    .hls { grid-template-columns: 1fr; }
    .rel { padding: 22px 20px; }
    .gh { margin-left: 0; }
  }
</style>
</head>
<body>

<div class="topbar">
  <div class="page in">
    <a class="brand" href="./" aria-label="Burrow">
      <svg class="mk" viewBox="0 0 100 100" aria-hidden="true"><rect x="2" y="2" width="96" height="96" rx="26" fill="#241B12"/><path d="M27 63 a23 23 0 0 1 46 0 Z" fill="#F3ECDD"/><rect x="40" y="63" width="20" height="6" rx="3" fill="#241B12"/></svg>
      <span class="nm">burrow</span>
    </a>
    <a class="back" href="./">← back to burrow</a>
    <a class="btn" href="__REPO__/releases/latest" target="_blank" rel="noopener">Download</a>
  </div>
</div>

<header class="hero page">
  <h1>Releases</h1>
  <p>Every version at a glance — what it added, fixed, and tightened. Full notes for each live on GitHub.</p>
  <p class="count">__COUNT__ releases · latest __LATEST__ · <a href="__REPO__/releases" target="_blank" rel="noopener" style="color:var(--ink-2); text-decoration:underline; text-underline-offset:2px;">GitHub releases ↗</a></p>
</header>

<main class="page">
__ENTRIES__
</main>

<footer>
  <div class="page">
    <span>Burrow — free, open-source, MIT.</span>
    <a href="./">Home</a>
    <a href="__REPO__" target="_blank" rel="noopener">GitHub</a>
    <a href="__REPO__/blob/main/SECURITY.md" target="_blank" rel="noopener">Security</a>
    <a href="__REPO__/blob/main/TELEMETRY.md" target="_blank" rel="noopener">Telemetry</a>
  </div>
</footer>
</body>
</html>
"""
    return (head.replace("__REPO__", REPO)
                .replace("__COUNT__", str(len(releases)))
                .replace("__LATEST__", esc(latest["version"]))
                .replace("__ENTRIES__", entries))


def patch_index(html, rel):
    begin = html.find(WN_BEGIN)
    end = html.find(WN_END)
    if begin == -1 or end == -1:
        sys.exit("index.html: WN:BEGIN / WN:END markers not found")
    html = html[:begin] + render_whatsnew(rel) + html[end + len(WN_END):]
    # Hero version badge.
    html = re.sub(r"(github\.com/caezium/Burrow</b> · )v[0-9][^<]*",
                  rf"\g<1>v{rel['version']}", html)
    return html


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--check", action="store_true",
                    help="verify generated files are up to date; exit 1 if not")
    ap.add_argument("--list-icons", action="store_true")
    args = ap.parse_args()

    if args.list_icons:
        print("icons: ", ", ".join(sorted(ICONS)))
        print("colors:", ", ".join(sorted(COLORS)))
        return

    releases = json.loads(DATA.read_text(encoding="utf-8"))["releases"]
    if not releases:
        sys.exit("releases.json: no releases")
    for r in releases:
        validate(r)
    versions = [r["version"] for r in releases]
    if sorted(versions, key=lambda v: [int(p) for p in v.split(".")], reverse=True) != versions:
        sys.exit("releases.json: releases must be ordered newest first")

    new_index = patch_index(INDEX.read_text(encoding="utf-8"), releases[0])
    new_history = render_history(releases)

    stale = []
    if INDEX.read_text(encoding="utf-8") != new_index:
        stale.append(str(INDEX.relative_to(ROOT)))
    if not HISTORY.exists() or HISTORY.read_text(encoding="utf-8") != new_history:
        stale.append(str(HISTORY.relative_to(ROOT)))

    if args.check:
        if stale:
            sys.exit(f"out of sync with docs/releases.json: {', '.join(stale)} "
                     f"— run python3 scripts/site-release.py and commit the result")
        print("site release content is in sync")
        return

    INDEX.write_text(new_index, encoding="utf-8")
    HISTORY.write_text(new_history, encoding="utf-8")
    print(f"regenerated: {', '.join(stale) if stale else 'nothing changed'}")


if __name__ == "__main__":
    main()
