#!/usr/bin/env python3
"""Render deterministic evidence charts from regenerated summary.json."""

from __future__ import annotations

import argparse
import json
import statistics
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

W, H = 1672, 941
BG = "#F7F8FB"
INK = "#111827"
MUTED = "#667085"
GRID = "#D9DEE8"
Q38 = "#2563EB"
Q36 = "#7C3AED"
G4 = "#E98A15"
LIGHT = "#E8ECF3"
TEAL = "#12998E"
MODELS = ["qwen38", "qwen36", "gemma4"]
NAMES = {"qwen38": "Qwen3.8-27B", "qwen36": "Qwen3.6-27B", "gemma4": "Gemma 4 31B-it"}
COLORS = {"qwen38": Q38, "qwen36": Q36, "gemma4": G4}
WEIGHTS_GIB = {"qwen38": 15.93, "qwen36": 15.66, "gemma4": 17.06}
TOTAL_GIB = 24564 / 1024


def font(size: int, bold: bool = False):
    path = "/System/Library/Fonts/Supplemental/Arial Bold.ttf" if bold else "/System/Library/Fonts/Supplemental/Arial.ttf"
    return ImageFont.truetype(path, size)


def canvas(title: str, subtitle: str):
    image = Image.new("RGB", (W, H), BG)
    draw = ImageDraw.Draw(image)
    draw.text((76, 56), title, fill=INK, font=font(52, True))
    draw.text((78, 124), subtitle, fill=MUTED, font=font(25))
    return image, draw


def footer(draw: ImageDraw.ImageDraw, text: str):
    draw.line((76, 872, W - 76, 872), fill=GRID, width=2)
    draw.text((76, 888), text, fill=MUTED, font=font(20))


def save(image: Image.Image, out: Path):
    out.parent.mkdir(parents=True, exist_ok=True)
    image.save(out, "PNG", optimize=True)


def context_row(summary, model, ctx, best=False):
    key = "best_fit_context" if best else "equal_q4_f16_context"
    rows = summary["models"][model]["performance"][key]
    return next((row for row in rows if row["ctx_allocated"] == ctx), None)


def memory_chart(summary, out: Path):
    image, draw = canvas("What fits inside 24GB?", "Measured peak VRAM at 32K equal-Q4 and 64K best-fit profiles")
    # Reserve a fixed right gutter for peak labels so they stay inside canvas.
    left, top, chart_w, chart_h = 160, 230, 1280, 520
    for tick in [0, 4, 8, 12, 16, 20, 24]:
        x = left + chart_w * tick / 24
        draw.line((x, top, x, top + chart_h), fill=GRID, width=2)
        draw.text((x - 14, top + chart_h + 12), str(tick), fill=MUTED, font=font(20))
    draw.text((left + chart_w - 90, top + chart_h + 48), "GiB", fill=MUTED, font=font(20))
    groups = [("32K · F16 K/V", 32768, False), ("64K · best fit", 65536, True)]
    y = top + 14
    for group_name, ctx, best in groups:
        draw.text((76, y + 20), group_name, fill=INK, font=font(22, True))
        for model in MODELS:
            row = context_row(summary, model, ctx, best=(best and model == "gemma4")) or context_row(summary, model, ctx, best=False)
            yy = y + 70
            draw.text((76, yy + 8), NAMES[model], fill=INK, font=font(19))
            bx = left
            bw = chart_w * WEIGHTS_GIB[model] / 24
            peak = row["peak_gpu_mib"] / 1024
            runtime = max(0, peak - WEIGHTS_GIB[model])
            rw = chart_w * runtime / 24
            hw = chart_w * max(0, TOTAL_GIB - peak) / 24
            draw.rounded_rectangle((bx, yy, bx + bw, yy + 38), radius=5, fill=COLORS[model])
            draw.rectangle((bx + bw, yy, bx + bw + rw, yy + 38), fill=TEAL)
            draw.rounded_rectangle((bx + bw + rw, yy, bx + bw + rw + hw, yy + 38), radius=5, fill=LIGHT)
            draw.text((bx + 10, yy + 8), f"{WEIGHTS_GIB[model]:.2f}", fill="white", font=font(18, True))
            draw.text((bx + bw + rw + 8, yy + 8), f"{max(0,TOTAL_GIB-peak):.2f} free", fill=MUTED, font=font(17, True))
            draw.text((left + chart_w + 12, yy + 7), f"{peak:.2f} peak", fill=INK, font=font(18, True))
            y += 55
        y += 40
    draw.rectangle((690, 184, 718, 208), fill=Q38); draw.text((727, 183), "Weights (model colour)", fill=MUTED, font=font(19))
    draw.rectangle((1000, 184, 1028, 208), fill=TEAL); draw.text((1037, 183), "Runtime + cache", fill=MUTED, font=font(19))
    draw.rectangle((1250, 184, 1278, 208), fill=LIGHT); draw.text((1287, 183), "Headroom", fill=MUTED, font=font(19))
    draw.text((78, 820), "Gemma 4 at 64K with F16 K/V: CUDA OOM. The shown best-fit result uses Q8_0 K/V and passed three turns.", fill=G4, font=font(22, True))
    footer(draw, "Kingy.ai measured · Q4_K_M weights · RTX 4090 24,564 MiB · llama.cpp b10453 · August 16, 2026")
    save(image, out)


def speed_chart(summary, out: Path):
    image, draw = canvas("Same GPU, different throughput", "Median of five llama-bench repetitions; prompt processing and decode are separate")
    plot = (105, 250, 940, 735)
    x0, y0, x1, y1 = plot
    max_y = 3200
    for tick in [0, 800, 1600, 2400, 3200]:
        y = y1 - (y1 - y0) * tick / max_y
        draw.line((x0, y, x1, y), fill=GRID, width=2)
        draw.text((38, y - 12), str(tick), fill=MUTED, font=font(19))
    xs = [260, 520, 800]
    conditions = ["prompt_512", "prompt_8192", "prompt_32768"]
    labels = ["512", "8,192", "32,768"]
    for model in MODELS:
        points = []
        for x, condition in zip(xs, conditions):
            value = summary["models"][model]["performance"]["synthetic"][condition]["median_tokens_per_second"]
            y = y1 - (y1 - y0) * value / max_y
            points.append((x, y))
        draw.line(points, fill=COLORS[model], width=6)
        for x, y in points:
            draw.ellipse((x - 8, y - 8, x + 8, y + 8), fill=COLORS[model])
    for x, label in zip(xs, labels):
        draw.text((x - 28, y1 + 14), label, fill=INK, font=font(20))
    draw.text((355, y1 + 53), "Prompt tokens", fill=MUTED, font=font(21))
    draw.text((38, y0 - 40), "Prompt tok/s", fill=MUTED, font=font(21))
    for i, model in enumerate(MODELS):
        draw.line((130 + i * 280, 190, 180 + i * 280, 190), fill=COLORS[model], width=7)
        draw.text((190 + i * 280, 176), NAMES[model], fill=INK, font=font(19, True))
    bx0, by0, bx1, by1 = 1070, 250, 1570, 735
    draw.text((1070, 204), "Decode · 256 tokens", fill=INK, font=font(25, True))
    max_decode = 55
    bar_w = 105
    for i, model in enumerate(MODELS):
        value = summary["models"][model]["performance"]["synthetic"]["decode_256"]["median_tokens_per_second"]
        x = bx0 + 38 + i * 150
        h = (by1 - by0) * value / max_decode
        draw.rounded_rectangle((x, by1 - h, x + bar_w, by1), radius=8, fill=COLORS[model])
        draw.text((x + 12, by1 - h - 34), f"{value:.2f}", fill=INK, font=font(20, True))
        short = ["Qwen3.8", "Qwen3.6", "Gemma 4"][i]
        draw.text((x - 2, by1 + 14), short, fill=INK, font=font(18))
    q_decode = statistics.mean(
        summary["models"][model]["performance"]["synthetic"]["decode_256"]["median_tokens_per_second"]
        for model in ("qwen38", "qwen36")
    )
    gemma_decode = summary["models"]["gemma4"]["performance"]["synthetic"]["decode_256"]["median_tokens_per_second"]
    slower = 100 * (1 - gemma_decode / q_decode)
    draw.text((1080, 785), "Qwen3.8 vs Qwen3.6: practical tie", fill=Q38, font=font(23, True))
    draw.text((1080, 819), f"Gemma 4: {slower:.1f}% slower decode", fill=G4, font=font(22, True))
    footer(draw, "Kingy.ai measured · Q4_K_M · full GPU offload · F16 K/V · flash attention on · RTX 4090")
    save(image, out)


def quality_chart(summary, out: Path):
    image, draw = canvas("Reliability by workload", "Counts and Wilson 95% intervals; no weighted overall score")
    measures = [
        ("Instruction", "instruction"), ("Reasoning runs", "reasoning_runs"),
        ("Single tools", "tools_single_runs"), ("Multi-step tools", "tools_multi_runs"),
        ("Coding pass@1", "coding_pass_at_1"), ("Document QA", "document_qa"), ("Vision", "vision"),
    ]
    left, top, width = 420, 215, 1090
    for tick in [0, 25, 50, 75, 100]:
        x = left + width * tick / 100
        draw.line((x, top, x, 820), fill=GRID, width=2)
        draw.text((x - 13, 830), str(tick), fill=MUTED, font=font(18))
    for i, (label, key) in enumerate(measures):
        cy = top + 30 + i * 84
        draw.text((76, cy + 18), label, fill=INK, font=font(22, True))
        for j, model in enumerate(MODELS):
            row = summary["models"][model]["quality"][key]
            y = cy + j * 18
            rate = row["rate"] * 100
            low = row["low"] * 100; high = row["high"] * 100
            x = left + width * rate / 100
            xl = left + width * low / 100; xh = left + width * high / 100
            draw.line((xl, y + 7, xh, y + 7), fill=COLORS[model], width=3)
            draw.ellipse((x - 6, y + 1, x + 6, y + 13), fill=COLORS[model])
            draw.text((x + 10, y - 4), f"{row['successes']}/{row['total']}", fill=COLORS[model], font=font(16, True))
    for i, model in enumerate(MODELS):
        draw.ellipse((710 + i * 280, 178, 726 + i * 280, 194), fill=COLORS[model])
        draw.text((735 + i * 280, 174), NAMES[model], fill=INK, font=font(18, True))
    draw.text((1325, 846), "Pass rate (%)", fill=MUTED, font=font(18))
    footer(draw, "Kingy.ai measured · identical synthetic fixtures · errors/timeouts retained · coding pass@1 uses first declared seed")
    save(image, out)


def paired_delta_chart(summary, out: Path):
    image, draw = canvas("Qwen3.8 vs Qwen3.6: paired deltas", "Qwen3.8-only passes to the right; Qwen3.6-only passes to the left; ties shown as counts")
    rows = summary["paired"]["qwen38_vs_qwen36"]
    order = ["instruction", "reasoning", "tools_single", "tools_multi", "coding", "documents", "vision", "writing", "multilingual"]
    labels = {"instruction":"Instruction", "reasoning":"Reasoning runs", "tools_single":"Single tools", "tools_multi":"Multi-step tools", "coding":"Coding runs", "documents":"Document QA", "vision":"Vision", "writing":"Writing checks", "multilingual":"Multilingual checks"}
    center, scale = 835, 460
    draw.line((center, 205, center, 800), fill=INK, width=3)
    draw.text((210, 174), "Qwen3.6-only", fill=Q36, font=font(21, True))
    draw.text((1300, 174), "Qwen3.8-only", fill=Q38, font=font(21, True))
    for i, key in enumerate(order):
        if key not in rows: continue
        row = rows[key]; y = 235 + i * 63
        total = max(1, row["paired_items"])
        left = row["right_only_pass"] / total
        right = row["left_only_pass"] / total
        draw.text((76, y - 5), labels[key], fill=INK, font=font(20, True))
        draw.rounded_rectangle((center - scale * left, y, center, y + 28), radius=5, fill=Q36)
        draw.rounded_rectangle((center, y, center + scale * right, y + 28), radius=5, fill=Q38)
        if row["right_only_pass"]:
            draw.text((center - scale * left - 36, y + 3), str(row["right_only_pass"]), fill=Q36, font=font(18, True))
        if row["left_only_pass"]:
            draw.text((center + scale * right + 10, y + 3), str(row["left_only_pass"]), fill=Q38, font=font(18, True))
        ties = row["both_pass"] + row["both_fail"]
        draw.text((1340, y + 2), f"ties {ties}/{total} · p={row['mcnemar_exact_two_sided_p']:.3f}", fill=MUTED, font=font(17))
    draw.text((500, 820), "← Qwen3.6 advantage", fill=Q36, font=font(18))
    draw.text((900, 820), "Qwen3.8 advantage →", fill=Q38, font=font(18))
    footer(draw, "Paired exact outcomes · two-sided exact McNemar p-values · ties include both-pass and both-fail")
    save(image, out)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--summary", type=Path, required=True)
    parser.add_argument("--out", type=Path, required=True)
    args = parser.parse_args()
    summary = json.loads(args.summary.read_text())
    memory_chart(summary, args.out / "qwen38-qwen36-gemma4-memory-context.png")
    speed_chart(summary, args.out / "qwen38-qwen36-gemma4-speed.png")
    quality_chart(summary, args.out / "qwen38-qwen36-gemma4-quality.png")
    paired_delta_chart(summary, args.out / "qwen38-vs-qwen36-paired-deltas.png")
    print(json.dumps({"out": str(args.out), "charts": 4}))


if __name__ == "__main__":
    main()
