#!/usr/bin/env python3
"""
Generate per-page OG images for RankWizAI marketing routes.

Editorial-product / operator's-tool template:
- Pure white background
- 1px slate-200 hairline rules top and bottom (real dividers, not decoration)
- Brand chrome top-left (icon + wordmark), mono route slug top-right
- Asymmetric headline: large editorial sans, left-aligned, single emphasis
- Supporting clause beneath in slate-500
- Bottom chrome: rankwizai.com (mono, left), brand line (mono, right)
- 4px brand-purple accent rule at the bottom edge

Each page entry produces /public/og/og-{slug}-v2.svg and /public/og/og-{slug}-v2.png.
PNG is the file referenced from Inertia <Head>; SVG is kept as a versionable master.

Usage:
    python3 scripts/branding/generate_og_images.py
"""

from __future__ import annotations

import json
import shutil
import subprocess
import sys
from html import escape
from pathlib import Path

import cairosvg

REPO_ROOT = Path(__file__).resolve().parents[2]
PUBLIC_DIR = REPO_ROOT / "public"
OG_DIR = PUBLIC_DIR / "og"
CROWN_SPEC_PATH = REPO_ROOT / "resources" / "branding" / "crown.json"


def load_crown_spec() -> dict:
    """
    Load the canonical crown geometry + color tokens from
    resources/branding/crown.json — the same file consumed by Logo.tsx and
    OgImageRenderer.php. Single source of truth across all three implementations.
    """
    with CROWN_SPEC_PATH.open(encoding="utf-8") as f:
        return json.load(f)


CROWN = load_crown_spec()


def preflight_fonts() -> None:
    """
    Verify Inter + JetBrains Mono are discoverable by fontconfig before we
    render anything. Without these, cairosvg silently falls back to Lato +
    DejaVu Sans Mono — visually similar but not pixel-equivalent. The drift
    only shows up when comparing OG cards across CI vs local renders, which
    is exactly the kind of bug nobody catches until production.

    On miss: print clear remediation instructions and exit non-zero so CI
    fails loudly rather than shipping fonts-mismatched cards.
    """
    if shutil.which("fc-match") is None:
        # No fontconfig — non-Linux dev machines. Skip the check; the
        # generator still works (renders with whatever's available).
        return

    required = {
        "Inter:weight=800": "Inter",
        "JetBrains Mono": "JetBrainsMono",
    }
    missing: list[str] = []
    for query, expected_substring in required.items():
        result = subprocess.run(
            ["fc-match", query],
            capture_output=True, text=True, check=False,
        )
        if expected_substring.lower() not in result.stdout.lower():
            missing.append(f"  - {query!r} → got {result.stdout.strip() or '(nothing)'}")

    if missing:
        print(
            "ERROR: required fonts missing — OG images would render with the wrong typography.\n"
            "Missing:\n" + "\n".join(missing) + "\n\n"
            "Install (Linux):\n"
            "  mkdir -p ~/.local/share/fonts/inter\n"
            "  # drop Inter-{Regular,Medium,SemiBold,Bold,ExtraBold,Black}.ttf and\n"
            "  # JetBrainsMono-{Regular,Bold}.ttf into that directory, then:\n"
            "  fc-cache -f ~/.local/share/fonts/\n"
            "\n"
            "On macOS: install via Homebrew (`brew install --cask font-inter font-jetbrains-mono`)\n"
            "or drop the .ttf files into ~/Library/Fonts/.\n",
            file=sys.stderr,
        )
        sys.exit(1)

# ── Brand tokens (sourced from resources/branding/crown.json) ──
PURPLE = CROWN["colors"]["purple"]
PURPLE_DEEP = CROWN["colors"]["purpleDeep"]
AMBER = CROWN["colors"]["amber"]
SLATE_900 = CROWN["colors"]["slate900"]
SLATE_500 = CROWN["colors"]["slate500"]
SLATE_200 = CROWN["colors"]["slate200"]
WHITE = CROWN["colors"]["white"]


def crown_svg_group(*, body: str, band: str, gem: str, sparkle: str) -> str:
    """
    Inline crown markup (no <svg> wrapper) at native 120×120 coords.
    Geometry comes from crown.json so Logo.tsx, OgImageRenderer.php, and
    this generator all draw the same crown.
    """
    body_path = f'<path d="{CROWN["paths"]["body"]}" fill="{body}"/>'
    b = CROWN["band"]
    band_rect = f'<rect x="{b["x"]}" y="{b["y"]}" width="{b["width"]}" height="{b["height"]}" fill="{band}"/>'
    gems = "".join(
        f'<circle cx="{g["cx"]}" cy="{g["cy"]}" r="{g["r"]}" fill="{gem}"/>'
        for g in CROWN["gems"]
    )
    sparkle_path = f'<path d="{CROWN["paths"]["sparkle"]}" fill="{sparkle}"/>'
    return body_path + band_rect + gems + sparkle_path


# Top-left brand lockup at 1200×630 OG scale.
# Icon at 32×32 left of wordmark; baseline aligned to icon's optical center.
TOP_LOCKUP_TPL = """  <g transform="translate(80, 88)">
    <g transform="scale(0.2667)">
      {crown}
    </g>
    <text x="44" y="25" font-family="Inter, 'Inter Display', sans-serif"
          font-weight="700" font-size="22" letter-spacing="-0.5" fill="{ink}">RankWizAI</text>
  </g>"""


OG_TEMPLATE = """<svg viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
  <!-- BG -->
  <rect width="1200" height="630" fill="{bg}"/>

  <!-- Top hairline divider -->
  <line x1="80" y1="120" x2="1120" y2="120" stroke="{rule}" stroke-width="1"/>

  <!-- Bottom hairline divider -->
  <line x1="80" y1="540" x2="1120" y2="540" stroke="{rule}" stroke-width="1"/>

  {top_lockup}

  <!-- Top-right route slug (mono) -->
  <text x="1120" y="103" text-anchor="end"
        font-family="'JetBrains Mono', 'DejaVu Sans Mono', monospace"
        font-size="16" font-weight="500" fill="{slug_ink}" letter-spacing="0.5">{slug}</text>

  <!-- Eyebrow tag (mono, optional) -->
  {eyebrow}

  <!-- Headline -->
  {headline}

  <!-- Subhead -->
  {subhead}

  <!-- Bottom-left site URL (mono) -->
  <text x="80" y="582" font-family="'JetBrains Mono', 'DejaVu Sans Mono', monospace"
        font-size="14" font-weight="500" fill="{slug_ink}" letter-spacing="0.5">rankwizai.com</text>

  <!-- Bottom-right brand line (mono) -->
  <text x="1120" y="582" text-anchor="end" font-family="'JetBrains Mono', 'DejaVu Sans Mono', monospace"
        font-size="14" font-weight="500" fill="{slug_ink}" letter-spacing="0.5">Diagnose. Fix. Prove the ROI.</text>

  <!-- Brand purple accent rule, full bleed -->
  <rect x="0" y="622" width="1200" height="8" fill="{accent}"/>
</svg>"""


def render_eyebrow(eyebrow: str | None, *, ink: str) -> str:
    if not eyebrow:
        return ""
    return (
        f'<text x="80" y="200" font-family="\'JetBrains Mono\', \'DejaVu Sans Mono\', monospace" '
        f'font-size="15" font-weight="600" fill="{ink}" letter-spacing="2">{escape(eyebrow.upper())}</text>'
    )


def render_headline(headline: str, *, y_start: int, ink: str, size: int = 96) -> str:
    """
    Render a multi-line headline. Each newline starts a new line.
    Lines are tightly stacked (1.0 leading) to read as one editorial unit.
    """
    lines = headline.split("\n")
    leading = int(size * 1.05)
    out = []
    y = y_start
    for line in lines:
        out.append(
            f'<text x="80" y="{y}" font-family="\'Inter Display\', Inter, sans-serif" '
            f'font-weight="800" font-size="{size}" letter-spacing="-3" fill="{ink}">{escape(line)}</text>'
        )
        y += leading
    return "\n  ".join(out)


def render_subhead(subhead: str, *, y: int, ink: str) -> str:
    if not subhead:
        return ""
    return (
        f'<text x="80" y="{y}" font-family="Inter, sans-serif" font-weight="400" '
        f'font-size="26" fill="{ink}" letter-spacing="-0.2">{escape(subhead)}</text>'
    )


def measure_headline_block(headline: str, *, size: int) -> int:
    """Return total vertical pixels consumed by the headline block."""
    leading = int(size * 1.05)
    return leading * len(headline.split("\n"))


def build_og_svg(*, slug: str, headline: str, subhead: str, eyebrow: str | None = None,
                 size: int = 96) -> str:
    # Compute vertical layout. Headline starts after the top-rule + eyebrow space.
    eyebrow_offset = 0 if not eyebrow else 32
    headline_y_start = 252 + eyebrow_offset
    headline_height = measure_headline_block(headline, size=size)
    subhead_y = headline_y_start + headline_height + 24

    # Auto-shrink very long headlines to keep within the bottom rule.
    if subhead_y > 510:
        size = 84
        eyebrow_offset = 0 if not eyebrow else 28
        headline_y_start = 244 + eyebrow_offset
        headline_height = measure_headline_block(headline, size=size)
        subhead_y = headline_y_start + headline_height + 24
    if subhead_y > 510:
        size = 72
        headline_y_start = 232 + eyebrow_offset
        headline_height = measure_headline_block(headline, size=size)
        subhead_y = headline_y_start + headline_height + 24

    crown = crown_svg_group(body=PURPLE, band=PURPLE_DEEP, gem=AMBER, sparkle=AMBER)
    top_lockup = TOP_LOCKUP_TPL.format(crown=crown, ink=SLATE_900)

    return OG_TEMPLATE.format(
        bg=WHITE,
        rule=SLATE_200,
        slug_ink=SLATE_500,
        accent=PURPLE,
        slug=escape(slug),
        top_lockup=top_lockup,
        eyebrow=render_eyebrow(eyebrow, ink=PURPLE),
        headline=render_headline(headline, y_start=headline_y_start, ink=SLATE_900, size=size),
        subhead=render_subhead(subhead, y=subhead_y, ink=SLATE_500),
    )


def write_og(name: str, *, slug: str, headline: str, subhead: str,
             eyebrow: str | None = None) -> None:
    OG_DIR.mkdir(parents=True, exist_ok=True)
    svg = build_og_svg(slug=slug, headline=headline, subhead=subhead, eyebrow=eyebrow)
    svg_path = OG_DIR / f"og-{name}-v2.svg"
    png_path = OG_DIR / f"og-{name}-v2.png"
    svg_path.write_text(svg, encoding="utf-8")
    cairosvg.svg2png(bytestring=svg.encode("utf-8"),
                     write_to=str(png_path),
                     output_width=1200, output_height=630)
    size = png_path.stat().st_size
    print(f"  → og-{name}-v2.png  {size/1024:.1f} KB")


# ── Page configurations ────────────────────────────────────────────────────
# Headlines use \n to control line breaks. Keep lines short (<= 14 chars at
# 96px) so the editorial layout stays asymmetric without crowding the right
# rail. Subheads cap at ~80 characters.
PAGES = [
    # Wave 2 — top 5
    {"name": "welcome",  "slug": "/",
     "eyebrow": "Real GSC data · AI rewrites · ROI proof",
     "headline": "Diagnose.\nFix. Prove\nthe ROI.",
     "subhead": "The WordPress SEO tool that closes the loop between analysis and proof."},

    {"name": "pricing", "slug": "/pricing",
     "eyebrow": "Free to start · BYOK pricing",
     "headline": "Pricing\nthat scales\nwith your sites.",
     "subhead": "Bring your own OpenAI key. No per-seat tax. Cancel any time."},

    {"name": "features", "slug": "/features",
     "eyebrow": "GSC sync · AI rewrites · ROI tracking",
     "headline": "One workflow.\nNot a stack\nof tabs.",
     "subhead": "Diagnose drops, generate rewrites, publish to WordPress, measure ROI."},

    {"name": "free-audit", "slug": "/free-audit",
     "eyebrow": "Free · No credit card",
     "headline": "Free SEO audit.\nFor your real\nGSC data.",
     "subhead": "Connect Search Console in 60 seconds. See what's hurting your rankings."},

    {"name": "use-cases-agencies", "slug": "/use-cases/agencies",
     "eyebrow": "For agencies",
     "headline": "Every client.\nEvery site.\nOne dashboard.",
     "subhead": "Track before/after ROI on every change you publish — across the whole book."},

    # Wave 3 — remaining marketing pages
    {"name": "use-cases-bloggers", "slug": "/use-cases/bloggers",
     "eyebrow": "For independent publishers",
     "headline": "Stop guessing\nwhich posts\nare decaying.",
     "subhead": "RankWizAI watches every page's GSC trajectory and flags the ones that need a rewrite."},

    {"name": "use-cases-ecommerce", "slug": "/use-cases/ecommerce",
     "eyebrow": "For ecommerce",
     "headline": "Category pages\nthat hold\ntheir rankings.",
     "subhead": "Detect cannibalization, refresh thin content, prove revenue impact per change."},

    {"name": "use-cases-consultants", "slug": "/use-cases/consultants",
     "eyebrow": "For SEO consultants",
     "headline": "Hand clients\nbefore/after\nproof.",
     "subhead": "Every recommendation tracked. Every outcome measured. Every report shippable."},

    {"name": "roadmap", "slug": "/roadmap",
     "eyebrow": "Public roadmap",
     "headline": "What's shipping.\nWhat's next.\nIn the open.",
     "subhead": "Vote on features, see what's in flight, get notified when it lands."},

    {"name": "changelog", "slug": "/changelog",
     "eyebrow": "Changelog",
     "headline": "Every change,\non the record.",
     "subhead": "Atom feed available. Subscribe to keep up with what's new."},

    {"name": "press", "slug": "/press",
     "eyebrow": "Press",
     "headline": "Press kit\nand assets.",
     "subhead": "Logos, screenshots, founder bios, brand colors. Hot-linkable, version-controlled."},

    {"name": "case-studies", "slug": "/case-studies",
     "eyebrow": "Case studies",
     "headline": "Real customers.\nReal traffic.\nReal proof.",
     "subhead": "Before/after GSC data from the agencies and publishers using RankWizAI."},

    {"name": "learn", "slug": "/learn",
     "eyebrow": "Learn",
     "headline": "How to fix\nwhat's broken\nin your SEO.",
     "subhead": "Field guides, frameworks, and walkthroughs for working SEO professionals."},

    {"name": "blog", "slug": "/blog",
     "eyebrow": "Blog",
     "headline": "Notes from\nthe SEO\noperator's desk.",
     "subhead": "Diagnostics, frameworks, and post-mortems from the team building RankWizAI."},

    {"name": "compare", "slug": "/compare",
     "eyebrow": "Comparisons",
     "headline": "RankWizAI\nvs. the field.",
     "subhead": "How we compare on diagnostics, AI quality, ROI proof, and price."},

    {"name": "privacy", "slug": "/privacy",
     "eyebrow": "Privacy",
     "headline": "Your data\nis yours.",
     "subhead": "We don't train on your content. We don't sell. We don't track without consent."},

    {"name": "terms", "slug": "/terms",
     "eyebrow": "Terms of service",
     "headline": "Terms of\nservice.",
     "subhead": "The legal basis for using RankWizAI. Plain language where possible."},

    {"name": "dpa", "slug": "/dpa",
     "eyebrow": "Data processing agreement",
     "headline": "Data\nprocessing\nagreement.",
     "subhead": "GDPR-compliant DPA available to all customers. Sign electronically."},

    {"name": "sub-processors", "slug": "/sub-processors",
     "eyebrow": "Sub-processors",
     "headline": "Sub-\nprocessors.",
     "subhead": "Every third party that touches your data, with their role and jurisdiction."},

    # Default fallback (replaces og-image.png)
    {"name": "default", "slug": "rankwizai.com",
     "eyebrow": "Real GSC data · AI rewrites · ROI proof",
     "headline": "Diagnose.\nFix. Prove\nthe ROI.",
     "subhead": "The WordPress SEO tool that closes the loop between analysis and proof."},
]


def main() -> None:
    preflight_fonts()
    print(f"Generating {len(PAGES)} OG images → {OG_DIR}")
    for page in PAGES:
        write_og(**page)
    print("Done.")


if __name__ == "__main__":
    main()
