from __future__ import annotations

import base64
import csv
import json
import math
import textwrap
import zipfile
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from PIL import Image as PILImage, ImageDraw, ImageFont
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import (
    SimpleDocTemplate,
    Paragraph,
    Spacer,
    Table,
    TableStyle,
    Image,
    PageBreak,
)

BASE = Path('/Users/nation/Documents/New project/artifacts/image-loadtest-20260501T030912Z')
RESPONSES = BASE / 'responses'
META = BASE / 'meta'
DECODED = BASE / 'decoded-images'
ASSET_DIR = BASE / 'report-assets'
OUT_PNG_DIR = BASE / 'report-pages'
OUT_PDF = BASE / 'committee-review-image-loadtest-20260501.pdf'
PUBLIC_DOWNLOAD_DIR = Path('/Users/nation/Documents/New project/public-chat-vrrp/public/downloads/mission-control/image-loadtest-20260501')
PUBLIC_ASSET_DIR = Path('/Users/nation/Documents/New project/public-chat-vrrp/public/assets')
ARTIFACT_ZIP = BASE / 'image-loadtest-20260501-artifacts.zip'
SUMMARY_JSON = BASE / 'mission-control-image-loadtest-20260501.json'
RESULTS_CSV = BASE / 'image-loadtest-results.csv'

for path in (DECODED, ASSET_DIR, OUT_PNG_DIR, PUBLIC_DOWNLOAD_DIR, PUBLIC_ASSET_DIR):
    path.mkdir(parents=True, exist_ok=True)

COLORS = {
    'ink': colors.HexColor('#122033'),
    'muted': colors.HexColor('#55657a'),
    'line': colors.HexColor('#d7dde5'),
    'accent': colors.HexColor('#155eb8'),
    'accent2': colors.HexColor('#c63f33'),
    'accent3': colors.HexColor('#d97706'),
    'panel': colors.HexColor('#f5f8fb'),
    'success': '#1f8f62',
    'failure': '#cf4b32',
    'neutral': '#4d6b8a',
}

SLUGS = [
    'hfimage__black-forest-labs_FLUX.1-schnell',
    'hfimage__Qwen_Qwen-Image',
    'hfimage__Qwen_Qwen-Image-Edit',
    'hfimage__black-forest-labs_FLUX.2-klein-4B',
    'hfimage__Tencent-Hunyuan_HunyuanDiT-v1.2-Diffusers',
    'hfimage__PixArt-alpha_PixArt-Sigma-XL-2-1024-MS',
    'hfimage__Qwen_Qwen-Image-Layered',
    'hfimage__stabilityai_stable-diffusion-xl-base-1.0',
    'hfimage__zai-org_CogView4-6B',
    'hfimage__black-forest-labs_FLUX.2-klein-base-4B',
]

MODEL_ORDER = {slug: idx for idx, slug in enumerate(SLUGS)}

NOTES = {
    'hfimage__black-forest-labs_FLUX.1-schnell': 'Fastest successful render.',
    'hfimage__Qwen_Qwen-Image': 'Base Qwen image model completed cleanly.',
    'hfimage__Qwen_Qwen-Image-Edit': 'Requires source image upload.',
    'hfimage__black-forest-labs_FLUX.2-klein-4B': 'Completed cleanly.',
    'hfimage__Tencent-Hunyuan_HunyuanDiT-v1.2-Diffusers': 'Failure window aligned with Spark GPU memory pressure.',
    'hfimage__PixArt-alpha_PixArt-Sigma-XL-2-1024-MS': 'Completed cleanly.',
    'hfimage__Qwen_Qwen-Image-Layered': 'Completed cleanly.',
    'hfimage__stabilityai_stable-diffusion-xl-base-1.0': 'Completed cleanly.',
    'hfimage__zai-org_CogView4-6B': 'Completed cleanly.',
    'hfimage__black-forest-labs_FLUX.2-klein-base-4B': 'Slowest successful render.',
}

DISPLAY_NAMES = {
    'hfimage__black-forest-labs_FLUX.1-schnell': 'FLUX.1-schnell',
    'hfimage__Qwen_Qwen-Image': 'Qwen Image',
    'hfimage__Qwen_Qwen-Image-Edit': 'Qwen Image Edit',
    'hfimage__black-forest-labs_FLUX.2-klein-4B': 'FLUX.2-klein-4B',
    'hfimage__Tencent-Hunyuan_HunyuanDiT-v1.2-Diffusers': 'HunyuanDiT v1.2',
    'hfimage__PixArt-alpha_PixArt-Sigma-XL-2-1024-MS': 'PixArt Sigma XL',
    'hfimage__Qwen_Qwen-Image-Layered': 'Qwen Image Layered',
    'hfimage__stabilityai_stable-diffusion-xl-base-1.0': 'Stable Diffusion XL',
    'hfimage__zai-org_CogView4-6B': 'CogView4-6B',
    'hfimage__black-forest-labs_FLUX.2-klein-base-4B': 'FLUX.2-klein-base-4B',
}


def fmt(num: float | int | None, digits: int = 2) -> str:
    if num is None:
        return 'n/a'
    return f'{num:.{digits}f}'


def human_gib(num_bytes: float) -> float:
    return round(num_bytes / 1024 / 1024 / 1024, 2)


def parse_prompt() -> str:
    return (BASE / 'prompt.txt').read_text().strip()


def load_vitals() -> list[dict[str, float | str]]:
    rows: list[dict[str, float | str]] = []
    with (BASE / 'spark_vitals.csv').open() as fh:
        reader = csv.DictReader(fh)
        for row in reader:
            rows.append({
                'timestamp_utc': row['timestamp_utc'],
                'gpu_util_pct': float(row['gpu_util_pct']),
                'temp_c': float(row['temp_c']),
                'power_w': float(row['power_w']),
            })
    return rows


def status_from_response(payload: dict) -> str:
    return 'success' if 'image' in payload else 'failed'


def load_results() -> list[dict]:
    results: list[dict] = []
    for slug in SLUGS:
        response_path = RESPONSES / f'{slug}.json'
        payload = json.loads(response_path.read_text())
        start_ms = int((META / f'{slug}.start_ms').read_text().strip())
        end_ms = int((META / f'{slug}.end_ms').read_text().strip())
        http_code = int((META / f'{slug}.http_code').read_text().strip())
        wall_seconds = round((end_ms - start_ms) / 1000, 2)
        status = status_from_response(payload)
        latency_ms = payload.get('latencyMs') if status == 'success' else None
        error = payload.get('error') if status != 'success' else None
        details = payload.get('details') if status != 'success' else None
        model = payload.get('model') if status == 'success' else slug.replace('hfimage__', 'hfimage::').replace('_', '/')
        results.append({
            'slug': slug,
            'model': model,
            'display_name': DISPLAY_NAMES[slug],
            'status': status,
            'wall_seconds': wall_seconds,
            'latency_ms': latency_ms,
            'http_code': http_code,
            'note': NOTES[slug],
            'error': error,
            'details': details,
            'payload': payload,
        })
    return results


def decode_images(results: list[dict]) -> list[dict]:
    decoded = []
    for row in results:
        payload = row['payload']
        image_meta = payload.get('image') if isinstance(payload, dict) else None
        if not image_meta:
            continue
        image_bytes = base64.b64decode(image_meta['base64'])
        out = DECODED / f"{row['slug']}.png"
        out.write_bytes(image_bytes)
        row['decoded_path'] = out
        row['asset_id'] = image_meta.get('assetId')
        row['image_size_bytes'] = image_meta.get('sizeBytes')
        decoded.append(row)
    return decoded


def font(size: int, bold: bool = False):
    candidates = [
        '/System/Library/Fonts/Supplemental/Arial Bold.ttf' if bold else '/System/Library/Fonts/Supplemental/Arial.ttf',
        '/System/Library/Fonts/Supplemental/Helvetica.ttc',
    ]
    for candidate in candidates:
        try:
            return ImageFont.truetype(candidate, size=size)
        except Exception:
            pass
    return ImageFont.load_default()


def make_contact_sheet(rows: list[dict]) -> Path:
    thumbs = []
    thumb_w, thumb_h = 380, 380
    label_h = 92
    cols = 2
    margin = 24
    pad = 18
    for row in rows:
        img = PILImage.open(row['decoded_path']).convert('RGB')
        canvas = PILImage.new('RGB', (thumb_w, thumb_h + label_h), '#0b1220')
        fitted = img.copy()
        fitted.thumbnail((thumb_w, thumb_h))
        x = (thumb_w - fitted.width) // 2
        y = (thumb_h - fitted.height) // 2
        canvas.paste(fitted, (x, y))
        draw = ImageDraw.Draw(canvas)
        draw.rectangle((0, thumb_h, thumb_w, thumb_h + label_h), fill='#111c31')
        draw.text((18, thumb_h + 12), row['display_name'], fill='white', font=font(28, bold=True))
        stats = f"{row['wall_seconds']:.2f}s | {('HTTP ' + str(row['http_code'])) if row['http_code'] else ''}".strip(' |')
        draw.text((18, thumb_h + 48), stats, fill='#b6c4dc', font=font(20))
        thumbs.append(canvas)

    rows_count = math.ceil(len(thumbs) / cols)
    sheet_w = margin * 2 + cols * thumb_w + (cols - 1) * pad
    sheet_h = margin * 2 + rows_count * (thumb_h + label_h) + (rows_count - 1) * pad + 90
    sheet = PILImage.new('RGB', (sheet_w, sheet_h), '#060910')
    draw = ImageDraw.Draw(sheet)
    draw.text((margin, 20), 'Spark Image Load Test | Successful Outputs', fill='white', font=font(42, bold=True))
    draw.text((margin, 64), 'Eight models completed under a 10-model concurrent launch through the public VIP path.', fill='#b6c4dc', font=font(22))

    for idx, thumb in enumerate(thumbs):
        r = idx // cols
        c = idx % cols
        x = margin + c * (thumb_w + pad)
        y = margin + 90 + r * (thumb_h + label_h + pad)
        sheet.paste(thumb, (x, y))

    out = ASSET_DIR / 'mission-control-image-loadtest-contact-sheet.png'
    sheet.save(out, quality=95)
    return out


def make_hero_banner(rows: list[dict]) -> Path:
    selected = rows[:4]
    banner = PILImage.new('RGB', (1600, 900), '#05070d')
    draw = ImageDraw.Draw(banner)
    positions = [(560, 56), (1080, 56), (560, 470), (1080, 470)]
    tile_w, tile_h = 420, 320
    for row, (x, y) in zip(selected, positions):
        img = PILImage.open(row['decoded_path']).convert('RGB')
        fitted = img.copy()
        fitted.thumbnail((tile_w, tile_h))
        tile = PILImage.new('RGB', (tile_w, tile_h), '#0d1627')
        tile.paste(fitted, ((tile_w - fitted.width)//2, (tile_h - fitted.height)//2))
        banner.paste(tile, (x, y))
        draw.rectangle((x, y + tile_h - 48, x + tile_w, y + tile_h), fill=(6, 9, 16, 220))
        draw.text((x + 14, y + tile_h - 37), row['display_name'], fill='white', font=font(24, bold=True))
    draw.rounded_rectangle((48, 48, 500, 852), radius=28, fill='#0b1323', outline='#294569', width=3)
    draw.text((82, 96), 'MISSION CONTROL', fill='#ffd88a', font=font(26, bold=True))
    draw.text((82, 148), 'Spark Image Model Sweep', fill='white', font=font(58, bold=True))
    draw.text((82, 276), '10 open-source image models launched at once through the public VIP path.', fill='#b6c4dc', font=font(30))
    draw.text((82, 374), 'Outcome', fill='#ffd88a', font=font(22, bold=True))
    draw.text((82, 408), '8 / 10 models succeeded', fill='white', font=font(34, bold=True))
    draw.text((82, 468), 'Peak Spark vitals', fill='#ffd88a', font=font(22, bold=True))
    draw.text((82, 502), '96% GPU util | 74C | 79.97W', fill='white', font=font(30, bold=True))
    draw.text((82, 564), 'Failure class', fill='#ffd88a', font=font(22, bold=True))
    draw.text((82, 598), 'Memory pressure, not GPU reset', fill='white', font=font(30, bold=True))
    draw.text((82, 700), 'Mission date: April 30, 2026', fill='#b6c4dc', font=font(24))
    draw.text((82, 740), 'Published on Yeti Claw Mission Control', fill='#b6c4dc', font=font(24))
    out = ASSET_DIR / 'mission-control-image-loadtest-banner.png'
    banner.save(out, quality=95)
    return out


def make_render_time_chart(results: list[dict]) -> Path:
    ordered = sorted(results, key=lambda row: row['wall_seconds'])
    fig, ax = plt.subplots(figsize=(8.6, 4.8), dpi=180)
    names = [row['display_name'] for row in ordered]
    vals = [row['wall_seconds'] for row in ordered]
    colors_list = [COLORS['success'] if row['status'] == 'success' else COLORS['failure'] for row in ordered]
    ax.barh(names, vals, color=colors_list)
    ax.set_title('Per-model time to finish under full 10-model burst', fontsize=12, loc='left')
    ax.set_xlabel('Wall time (seconds)', fontsize=10)
    ax.grid(True, axis='x', linewidth=0.4, alpha=0.3)
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.tick_params(labelsize=8.5)
    for idx, row in enumerate(ordered):
        ax.text(vals[idx] + 8, idx, row['status'], va='center', fontsize=8, color='#334155')
    fig.tight_layout()
    out = ASSET_DIR / 'mission-control-image-loadtest-render-times.png'
    fig.savefig(out, bbox_inches='tight')
    plt.close(fig)
    return out


def make_status_chart(results: list[dict]) -> Path:
    success = sum(1 for row in results if row['status'] == 'success')
    failure = len(results) - success
    fig, ax = plt.subplots(figsize=(4.4, 4.4), dpi=180)
    wedges, _ = ax.pie([success, failure], colors=[COLORS['success'], COLORS['failure']], startangle=90, wedgeprops=dict(width=0.42, edgecolor='white'))
    ax.text(0, 0.08, f'{success}/{len(results)}', ha='center', va='center', fontsize=20, fontweight='bold')
    ax.text(0, -0.12, 'completed', ha='center', va='center', fontsize=10, color='#55657a')
    ax.set_title('Run outcome mix', fontsize=12, loc='left')
    ax.legend(wedges, ['Success', 'Failure'], frameon=False, loc='lower left', bbox_to_anchor=(-0.05, -0.08), fontsize=9)
    out = ASSET_DIR / 'mission-control-image-loadtest-status-breakdown.png'
    fig.savefig(out, bbox_inches='tight')
    plt.close(fig)
    return out


def make_vitals_chart(vitals: list[dict]) -> Path:
    times = list(range(len(vitals)))
    util = [row['gpu_util_pct'] for row in vitals]
    temp = [row['temp_c'] for row in vitals]
    power = [row['power_w'] for row in vitals]

    fig, axes = plt.subplots(3, 1, figsize=(8.6, 6.6), dpi=180, sharex=True)
    series = [
        ('GPU utilization %', util, '#155eb8', axes[0]),
        ('GPU temperature C', temp, '#c63f33', axes[1]),
        ('GPU power W', power, '#d97706', axes[2]),
    ]
    for title, vals, color, ax in series:
        ax.plot(times, vals, linewidth=2.0, color=color)
        ax.set_title(title, fontsize=11, loc='left')
        ax.grid(True, axis='y', linewidth=0.4, alpha=0.3)
        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
        ax.tick_params(labelsize=8.5)
    axes[2].set_xlabel('5-second samples across the run', fontsize=10)
    fig.suptitle('Spark environmental envelope during the concurrent image sweep', fontsize=13, x=0.05, ha='left')
    fig.tight_layout(rect=(0, 0, 1, 0.97))
    out = ASSET_DIR / 'mission-control-image-loadtest-vitals.png'
    fig.savefig(out, bbox_inches='tight')
    plt.close(fig)
    return out


def build_styles():
    styles = getSampleStyleSheet()
    styles.add(ParagraphStyle(name='ReportTitle', fontName='Helvetica-Bold', fontSize=22, leading=26, textColor=COLORS['ink'], spaceAfter=10))
    styles.add(ParagraphStyle(name='ReportSub', fontName='Helvetica', fontSize=11, leading=14, textColor=COLORS['muted'], spaceAfter=12))
    styles.add(ParagraphStyle(name='Section', fontName='Helvetica-Bold', fontSize=14, leading=18, textColor=COLORS['ink'], spaceBefore=10, spaceAfter=8))
    styles.add(ParagraphStyle(name='Body', fontName='Helvetica', fontSize=10, leading=14, textColor=COLORS['ink'], spaceAfter=6))
    styles.add(ParagraphStyle(name='ReportBullet', fontName='Helvetica', fontSize=10, leading=14, textColor=COLORS['ink'], leftIndent=14, firstLineIndent=-10, spaceAfter=4))
    styles.add(ParagraphStyle(name='Callout', fontName='Helvetica', fontSize=10, leading=14, textColor=COLORS['ink'], backColor=COLORS['panel'], borderPadding=(8, 10, 8), borderColor=COLORS['line'], borderWidth=0.6, spaceAfter=8))
    return styles


def make_results_table(results: list[dict]) -> Table:
    data = [['Model', 'Status', 'Wall s', 'API ms', 'HTTP', 'Note']]
    for row in results:
        data.append([
            row['display_name'],
            row['status'],
            fmt(row['wall_seconds']),
            str(row['latency_ms']) if row['latency_ms'] is not None else 'n/a',
            str(row['http_code']),
            row['note'],
        ])
    table = Table(data, colWidths=[1.6*inch, 0.72*inch, 0.72*inch, 0.82*inch, 0.56*inch, 2.65*inch], repeatRows=1)
    table.setStyle(TableStyle([
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#122033')),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 8.5),
        ('LEADING', (0, 0), (-1, -1), 10),
        ('GRID', (0, 0), (-1, -1), 0.4, COLORS['line']),
        ('BACKGROUND', (0, 1), (-1, -1), colors.white),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8fafc')]),
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ]))
    return table


def build_pdf(results: list[dict], vitals: list[dict], contact_sheet: Path, banner: Path, render_chart: Path, status_chart: Path, vitals_chart: Path):
    styles = build_styles()
    doc = SimpleDocTemplate(str(OUT_PDF), pagesize=letter, leftMargin=0.55*inch, rightMargin=0.55*inch, topMargin=0.55*inch, bottomMargin=0.55*inch)

    success = [row for row in results if row['status'] == 'success']
    failures = [row for row in results if row['status'] != 'success']
    fastest = min(success, key=lambda row: row['wall_seconds'])
    slowest = max(success, key=lambda row: row['wall_seconds'])
    peak_gpu = max(v['gpu_util_pct'] for v in vitals)
    peak_temp = max(v['temp_c'] for v in vitals)
    peak_power = max(v['power_w'] for v in vitals)
    started = datetime.fromisoformat((BASE / 'start_utc.txt').read_text().strip().replace('Z', '+00:00'))

    story = []
    story.append(Image(str(banner), width=7.4*inch, height=4.16*inch))
    story.append(Spacer(1, 0.18*inch))
    story.append(Paragraph('Committee Review | Spark Image Model Sweep', styles['ReportTitle']))
    story.append(Paragraph(
        'Mission Control committee report for the 10-model concurrent open-source image generation sweep run through the public VIP path on April 30, 2026.',
        styles['ReportSub'],
    ))
    story.append(Paragraph(
        f'<b>Run window.</b> Started {started.strftime("%Y-%m-%d %H:%M:%S UTC")} | <b>Path.</b> chat.neonflux.co via 192.168.12.163 | <b>Prompt class.</b> realism / character generation.',
        styles['Body'],
    ))

    story.append(Paragraph('Executive summary', styles['Section']))
    story.append(Paragraph(
        f'<b>Outcome.</b> Eight of ten models completed successfully during a full 10-way concurrent burst. The two failures were operationally distinct: <b>Qwen Image Edit</b> failed because the request lacked a required source image, while <b>HunyuanDiT v1.2</b> failed inside a memory-pressure window. Spark stayed online, stayed reachable, and did not emit off-the-bus or full-chip reset signatures during the run.',
        styles['Callout'],
    ))
    story.append(Paragraph(f'• Fastest successful render: {fastest["display_name"]} in {fmt(fastest["wall_seconds"])} seconds.', styles['ReportBullet']))
    story.append(Paragraph(f'• Slowest successful render: {slowest["display_name"]} in {fmt(slowest["wall_seconds"])} seconds.', styles['ReportBullet']))
    story.append(Paragraph(f'• Peak Spark envelope: {fmt(peak_gpu,0)}% GPU utilization, {fmt(peak_temp,0)}C GPU temperature, {fmt(peak_power)}W power draw.', styles['ReportBullet']))
    story.append(Paragraph('• No GPU off-the-bus, full-chip reset, or reset-required signature was observed in the benchmark summary window.', styles['ReportBullet']))

    story.append(Paragraph('Test configuration', styles['Section']))
    setup_table = Table([
        ['Field', 'Value'],
        ['Path tested', 'http://192.168.12.163/api/chat with Host: chat.neonflux.co'],
        ['Launch style', '10 image models fired at once'],
        ['Prompt', 'Salvadoran realism prompt'],
        ['Spark samples', str(len(vitals))],
        ['HTTP outcome mix', '8 x 200 OK, 2 x 502 Bad Gateway'],
    ], colWidths=[1.7*inch, 5.2*inch])
    setup_table.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#122033')),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('GRID', (0,0), (-1,-1), 0.4, COLORS['line']),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor('#f8fafc')]),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('VALIGN', (0,0), (-1,-1), 'TOP'),
    ]))
    story.append(setup_table)
    story.append(Spacer(1, 0.16*inch))
    story.append(make_results_table(results))

    story.append(PageBreak())
    story.append(Paragraph('Performance curves', styles['Section']))
    story.append(Paragraph('The timing chart shows the true completion spread across the model set. The result is not a flat cluster: the fastest model finished in a little over three and a half minutes, while the slowest successful completion crossed thirteen minutes. That spread matters when deciding whether the public surface should allow free-form all-model bursts or should shape traffic into tiers.', styles['Body']))
    story.append(Image(str(render_chart), width=7.1*inch, height=3.95*inch))
    story.append(Spacer(1, 0.12*inch))
    story.append(Image(str(status_chart), width=3.1*inch, height=3.1*inch))

    story.append(Paragraph('Environmental telemetry', styles['Section']))
    story.append(Paragraph('Spark behaved like a loaded but healthy image factory. GPU utilization rose into the mid-90s, temperature peaked at 74C, and power climbed to nearly 80W before dropping back to idle once the queue drained. The important operational fact is that memory pressure showed up before thermal instability or transport loss.', styles['Body']))
    story.append(Image(str(vitals_chart), width=7.1*inch, height=5.45*inch))

    story.append(PageBreak())
    story.append(Paragraph('Output gallery', styles['Section']))
    story.append(Paragraph('The contact sheet below shows the eight successful outputs captured from the public path. These images were decoded directly from the response payloads saved during the run, not regenerated afterward.', styles['Body']))
    story.append(Image(str(contact_sheet), width=6.85*inch, height=9.68*inch))

    story.append(PageBreak())
    story.append(Paragraph('Operational opinion', styles['Section']))
    story.append(Paragraph('<b>What the test proves.</b> Spark can survive a 10-model image burst without transport collapse or GPU reset, but it does not mean every model mix is equally safe. The failing Hunyuan window is the signal to respect memory-heavy models as a separate tier.', styles['Callout']))
    story.append(Paragraph('• Good public-facing default lane: SDXL, PixArt, FLUX.2-klein-4B, and Qwen Image one at a time or in a shaped queue.', styles['ReportBullet']))
    story.append(Paragraph('• Keep edit-style pipelines behind capability checks because they require an input image and fail noisily without one.', styles['ReportBullet']))
    story.append(Paragraph('• Treat Hunyuan-class or similarly memory-hungry pipelines as premium jobs that should not be admitted blindly into a full free-for-all burst.', styles['ReportBullet']))
    story.append(Paragraph('• The absence of bus-reset signatures is a strong positive result. The bottleneck here was memory pressure, not host instability.', styles['ReportBullet']))

    doc.build(story)


def write_summary(results: list[dict], vitals: list[dict], contact_sheet: Path, banner: Path, render_chart: Path, status_chart: Path, vitals_chart: Path):
    success = [row for row in results if row['status'] == 'success']
    failures = [row for row in results if row['status'] != 'success']
    summary = {
        'run_started_utc': (BASE / 'start_utc.txt').read_text().strip(),
        'run_started_local': '2026-04-30 20:09:12 PDT',
        'path_tested': 'http://192.168.12.163/api/chat with Host: chat.neonflux.co',
        'concurrency': 10,
        'models_total': len(results),
        'success_count': len(success),
        'failure_count': len(failures),
        'peak_gpu_util_pct': max(v['gpu_util_pct'] for v in vitals),
        'peak_temp_c': max(v['temp_c'] for v in vitals),
        'peak_power_w': max(v['power_w'] for v in vitals),
        'samples_captured': len(vitals),
        'findings': {
            'gpu_reset_signature': False,
            'off_the_bus_signature': False,
            'primary_failure_class': 'GPU memory pressure during HunyuanDiT failure window',
            'secondary_failure_class': 'Input validation failure for Qwen Image Edit (missing source image)',
        },
        'artifacts': {
            'pdf': OUT_PDF.name,
            'contact_sheet': contact_sheet.name,
            'banner': banner.name,
            'render_chart': render_chart.name,
            'status_chart': status_chart.name,
            'vitals_chart': vitals_chart.name,
        },
        'results': [
            {
                'model': row['model'],
                'display_name': row['display_name'],
                'status': row['status'],
                'wall_seconds': row['wall_seconds'],
                'latency_ms': row['latency_ms'],
                'http_code': row['http_code'],
                'note': row['note'],
                'asset_id': row.get('asset_id'),
                'image_size_bytes': row.get('image_size_bytes'),
                'decoded_image': row.get('decoded_path').name if row.get('decoded_path') else None,
            }
            for row in results
        ],
    }
    SUMMARY_JSON.write_text(json.dumps(summary, indent=2))
    with RESULTS_CSV.open('w', newline='') as fh:
        writer = csv.writer(fh)
        writer.writerow(['display_name', 'model', 'status', 'wall_seconds', 'latency_ms', 'http_code', 'note', 'decoded_image'])
        for row in results:
            writer.writerow([row['display_name'], row['model'], row['status'], row['wall_seconds'], row['latency_ms'], row['http_code'], row['note'], row.get('decoded_path').name if row.get('decoded_path') else ''])


def build_zip():
    members = [
        BASE / 'SUMMARY.md',
        BASE / 'prompt.txt',
        BASE / 'models.txt',
        BASE / 'models.json',
        BASE / 'spark_vitals.csv',
        RESULTS_CSV,
        SUMMARY_JSON,
        OUT_PDF,
        BASE / 'build_committee_report.py',
    ]
    members.extend(sorted(DECODED.glob('*.png')))
    members.extend(sorted(ASSET_DIR.glob('*.png')))
    with zipfile.ZipFile(ARTIFACT_ZIP, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
        for path in members:
            zf.write(path, arcname=path.relative_to(BASE))


def copy_public_files(contact_sheet: Path, banner: Path, render_chart: Path, status_chart: Path, vitals_chart: Path, results: list[dict]):
    public_asset_map = {
        banner: PUBLIC_ASSET_DIR / 'mission-control-image-loadtest-banner.png',
        contact_sheet: PUBLIC_ASSET_DIR / 'mission-control-image-loadtest-contact-sheet.png',
        render_chart: PUBLIC_ASSET_DIR / 'mission-control-image-loadtest-render-times.png',
        status_chart: PUBLIC_ASSET_DIR / 'mission-control-image-loadtest-status-breakdown.png',
        vitals_chart: PUBLIC_ASSET_DIR / 'mission-control-image-loadtest-vitals.png',
    }
    for src, dst in public_asset_map.items():
        dst.write_bytes(src.read_bytes())

    for row in results:
        if row.get('decoded_path'):
            out = PUBLIC_ASSET_DIR / f"mission-control-{row['slug']}.png"
            out.write_bytes(row['decoded_path'].read_bytes())

    publish_files = [
        BASE / 'SUMMARY.md',
        BASE / 'prompt.txt',
        BASE / 'models.txt',
        BASE / 'models.json',
        BASE / 'spark_vitals.csv',
        RESULTS_CSV,
        SUMMARY_JSON,
        OUT_PDF,
        ARTIFACT_ZIP,
        BASE / 'build_committee_report.py',
    ]
    for src in publish_files:
        (PUBLIC_DOWNLOAD_DIR / src.name).write_bytes(src.read_bytes())


def main():
    results = load_results()
    decode_images(results)
    success_rows = [row for row in results if row['status'] == 'success']
    vitals = load_vitals()
    contact_sheet = make_contact_sheet(success_rows)
    banner = make_hero_banner(success_rows)
    render_chart = make_render_time_chart(results)
    status_chart = make_status_chart(results)
    vitals_chart = make_vitals_chart(vitals)
    write_summary(results, vitals, contact_sheet, banner, render_chart, status_chart, vitals_chart)
    build_pdf(results, vitals, contact_sheet, banner, render_chart, status_chart, vitals_chart)
    build_zip()
    copy_public_files(contact_sheet, banner, render_chart, status_chart, vitals_chart, results)
    print('wrote', OUT_PDF)
    print('wrote', ARTIFACT_ZIP)
    print('wrote', SUMMARY_JSON)


if __name__ == '__main__':
    main()
