#!/usr/bin/env python3
"""Draws the paper's SVG figures from ../results (standard library only).

Usage: python3 figures.py <output_dir>
"""
import glob
import json
import math
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
RES = os.path.join(HERE, "..", "results")
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(RES, "figures")
os.makedirs(OUT, exist_ok=True)
S = json.load(open(os.path.join(RES, "summary.json")))

INK, MUTED, GRID, BG = "#1a1d23", "#5b6170", "#e3e5ea", "#ffffff"
C_BAD, C_GOOD, C_ALT, C_NEUTRAL = "#c2410c", "#0f766e", "#4338ca", "#94a3b8"
FONT = "font-family='Inter, Helvetica, Arial, sans-serif'"


def svg(w, h, body, title, desc):
    return (f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {w} {h}' width='{w}' height='{h}' role='img' "
            f"aria-labelledby='t d'><title id='t'>{title}</title><desc id='d'>{desc}</desc>"
            f"<rect width='{w}' height='{h}' fill='{BG}'/>{body}</svg>\n")


def text(x, y, s, size=12, fill=INK, anchor="start", weight="normal"):
    return (f"<text x='{x:.1f}' y='{y:.1f}' {FONT} font-size='{size}' fill='{fill}' "
            f"text-anchor='{anchor}' font-weight='{weight}'>{s}</text>")


def jl(path):
    return [json.loads(l) for l in open(path) if l.strip().startswith("{")]


def logx(v, lo, hi, x0, x1):
    v = max(v, lo)
    return x0 + (math.log10(v) - math.log10(lo)) / (math.log10(hi) - math.log10(lo)) * (x1 - x0)


# Figure 1 (hero): lock scope. Every run as a dot, per variant and writer, log axis.
def fig_lockscope():
    variants = [("before", "Before fix (migrations to 010)"), ("after", "After fix (migrations to 011)"),
                ("head", "Current schema (per-account)"), ("head_immediate", "Current, SET CONSTRAINTS IMMEDIATE")]
    W, H = 640, 420
    x0, x1, lo, hi = 130, 610, 1, 10000
    body = [text(20, 26, "Wait for an audited write while another transaction holds the tenant's chain", 14, weight="600"),
            text(20, 44, "30 runs per row; the holder keeps its transaction open 5 s after its own audit write", 11, MUTED)]
    for t in (1, 10, 100, 1000, 10000):
        x = logx(t, lo, hi, x0, x1)
        body.append(f"<line x1='{x:.1f}' y1='58' x2='{x:.1f}' y2='{H-44}' stroke='{GRID}'/>")
        body.append(text(x, H - 28, f"{t:,} ms", 11, MUTED, "middle"))
    y = 62
    for v, label in variants:
        rows = jl(os.path.join(RES, f"exp1_lockscope_{v}.jsonl"))
        body.append(text(20, y + 12, label, 12, INK, weight="600"))
        for key, name, col, dy in (("same_tenant_ms", "same tenant", C_BAD, 26), ("other_tenant_ms", "other tenant", C_NEUTRAL, 46)):
            vals = sorted(r[key] for r in rows)
            body.append(text(x0 - 10, y + dy + 4, name, 11, MUTED, "end"))
            for i, val in enumerate(vals):
                jitter = ((i * 7) % 9 - 4) * 0.9
                body.append(f"<circle cx='{logx(val, lo, hi, x0, x1):.1f}' cy='{y + dy + jitter:.1f}' r='3' fill='{col}' fill-opacity='0.55'/>")
            med = vals[len(vals) // 2] if len(vals) % 2 else (vals[len(vals)//2 - 1] + vals[len(vals)//2]) / 2
            mx = logx(med, lo, hi, x0, x1)
            body.append(f"<line x1='{mx:.1f}' y1='{y + dy - 8}' x2='{mx:.1f}' y2='{y + dy + 8}' stroke='{INK}' stroke-width='2'/>")
        y += 74
    body.append(text(20, H - 6, "Vertical tick = median. Orange = writer in the holder's tenant; grey = writer in another tenant.", 11, MUTED))
    open(os.path.join(OUT, "lock-scope.svg"), "w").write(svg(W, H, "".join(body),
        "Audit chain lock scope before and after the fix",
        "Strip plot of commit latency for a same-tenant and an other-tenant audited write while a third transaction holds the tenant's audit chain open for five seconds, for four schema versions."))


# Figure 2: tenant-scope predicate coverage over schema history.
def fig_scope(points):
    W, H = 640, 300
    x0, x1, y0, y1 = 80, 610, 60, 240
    mx = max(p[2] for p in points) * 1.1
    body = [text(20, 26, "Tables inside the catalogue assertions' tenant scope, by schema version", 14, weight="600"),
            text(20, 44, "Old: the institution table or any table with institution_id. New: adds account and account_id.", 11, MUTED)]
    for t in range(0, int(mx) + 1, 20):
        y = y1 - t / mx * (y1 - y0)
        body.append(f"<line x1='{x0}' y1='{y:.1f}' x2='{x1}' y2='{y:.1f}' stroke='{GRID}'/>")
        body.append(text(x0 - 8, y + 4, str(t), 11, MUTED, "end"))
    bw = (x1 - x0) / len(points)
    for i, (label, old, new) in enumerate(points):
        cx = x0 + bw * i + bw / 2
        for j, (val, col) in enumerate(((old, C_NEUTRAL), (new, C_GOOD))):
            bx = cx - 34 + j * 36
            hgt = val / mx * (y1 - y0)
            body.append(f"<rect x='{bx:.1f}' y='{y1 - hgt:.1f}' width='32' height='{hgt:.1f}' fill='{col}'/>")
            body.append(text(bx + 16, y1 - hgt - 5, str(val), 11, INK, "middle"))
        if new > old:
            body.append(text(cx, y1 + 32, f"blind spot: {new - old}", 11, C_BAD, "middle", "600"))
        body.append(text(cx, y1 + 16, label, 11, INK, "middle"))
    body.append(f"<rect x='{x0}' y='{H-18}' width='10' height='10' fill='{C_NEUTRAL}'/>" + text(x0 + 14, H - 9, "old predicate", 11, MUTED))
    body.append(f"<rect x='{x0+120}' y='{H-18}' width='10' height='10' fill='{C_GOOD}'/>" + text(x0 + 134, H - 9, "new predicate", 11, MUTED))
    open(os.path.join(OUT, "scope-history.svg"), "w").write(svg(W, H, "".join(body),
        "Catalogue-assertion scope over schema history",
        "Grouped bars of how many public tables each tenant-scope predicate selects at migrations 016, 022 and the current head; the gap is the old predicate's blind spot."))


# Figure 3: RLS vs explicit tenancy filter, per query and scale.
def fig_rls():
    q = S["exp2_query_timing"]
    scales = [s for s in ("x0", "x50", "x1000") if s in q]
    labels = {"x0": "seed (22 events)", "x50": "x50 (1,122)", "x1000": "x1000 (22,022)"}
    W, H = 640, 320
    x0, x1, lo, hi = 190, 610, 0.01, 100000
    body = [text(20, 26, "Median server execution time per read: RLS versus explicit tenant filter", 14, weight="600"),
            text(20, 44, "16 personas x 5 runs (x1000: 4 personas x 1 run, so treat as indicative); filter = owner, account_id = $1", 11, MUTED)]
    for t in (0.01, 0.1, 1, 10, 100, 1000, 10000, 100000):
        x = logx(t, lo, hi, x0, x1)
        body.append(f"<line x1='{x:.1f}' y1='60' x2='{x:.1f}' y2='{H-46}' stroke='{GRID}'/>")
        body.append(text(x, H - 30, f"{t:g} ms" if t < 1000 else f"{t/1000:g} s", 11, MUTED, "middle"))
    y = 70
    for qname, qlabel in (("club", "Club's events (LIMIT 20)"), ("discovery", "Discovery list (LIMIT 50)")):
        body.append(text(20, y + 8, qlabel, 12, INK, weight="600"))
        y += 16
        for s in scales:
            d = q[s][qname]
            body.append(text(x0 - 10, y + 12, labels[s], 11, MUTED, "end"))
            for k, col, dy in (("owner_filter_exec_ms", C_NEUTRAL, 0), ("rls_exec_ms", C_BAD, 11)):
                v = d[k]["p50"]
                xe = logx(v, lo, hi, x0, x1)
                body.append(f"<rect x='{x0}' y='{y + dy + 2}' width='{max(1, xe - x0):.1f}' height='8' fill='{col}'/>")
                fmt = f"{v:.3f} ms" if v < 1 else (f"{v:.1f} ms" if v < 100 else (f"{v:,.0f} ms" if v < 10000 else f"{v/1000:.1f} s"))
                body.append(text(xe + 4, y + dy + 10, fmt, 11, INK))
            y += 26
        y += 6
    body.append(f"<rect x='{x0}' y='{H-18}' width='10' height='10' fill='{C_NEUTRAL}'/>" + text(x0 + 14, H - 9, "explicit filter (owner)", 11, MUTED))
    body.append(f"<rect x='{x0+170}' y='{H-18}' width='10' height='10' fill='{C_BAD}'/>" + text(x0 + 184, H - 9, "RLS policies (request role)", 11, MUTED))
    open(os.path.join(OUT, "rls-overhead.svg"), "w").write(svg(W, H, "".join(body),
        "RLS overhead by data scale",
        "Horizontal bars, log scale, of median execution time for two event reads under RLS and under an explicit account filter at three data scales."))


# Figure 4: helper formulations.
def fig_helpers():
    h = S.get("exp2_helper_overhead")
    if not h:
        return
    names = {"shipped_definer_with_set": "SECURITY DEFINER + SET search_path (shipped)",
             "definer_without_set": "SECURITY DEFINER, no SET",
             "invoker_inlinable": "SECURITY INVOKER, no SET",
             "set_based_definer_once": "definer called once, set-based",
             "no_membership_test": "no membership test (floor)",
             "university_admin_per_row": "row-independent role check, per row",
             "university_admin_hoisted": "same check wrapped in (select ...)"}
    W, H = 640, 310
    x0, x1, lo, hi = 300, 600, 1, 100000
    body = [text(20, 26, "Policy-helper styles over one account's 14,014 events", 14, weight="600"),
            text(20, 44, "Microbenchmark, not the shipped policies: median of 5 runs; each pair returns the same count", 11, MUTED)]
    for t in (1, 10, 100, 1000, 10000, 100000):
        x = logx(t, lo, hi, x0, x1)
        body.append(f"<line x1='{x:.1f}' y1='56' x2='{x:.1f}' y2='{H-34}' stroke='{GRID}'/>")
        body.append(text(x, H - 18, f"{t:g} ms" if t < 1000 else f"{t/1000:g} s", 11, MUTED, "middle"))
    y = 64
    for k, label in names.items():
        if k not in h:
            continue
        v = h[k]["exec_ms"]["p50"]
        xe = logx(v, lo, hi, x0, x1)
        col = C_BAD if k in ("shipped_definer_with_set", "university_admin_per_row") else (C_GOOD if k in ("set_based_definer_once", "university_admin_hoisted") else C_NEUTRAL)
        body.append(text(x0 - 10, y + 12, label, 11, INK, "end"))
        body.append(f"<rect x='{x0}' y='{y + 2}' width='{max(1, xe - x0):.1f}' height='14' fill='{col}'/>")
        body.append(text(xe + 4, y + 13, f"{v:,.0f} ms" if v >= 100 else f"{v:,.1f} ms", 11, INK))
        y += 30
    open(os.path.join(OUT, "helper-overhead.svg"), "w").write(svg(W, H, "".join(body),
        "Cost of the policy-helper style",
        "Bars, log scale, of median execution time for counting member events with five formulations of the same membership test and two of a row-independent role check."))


if __name__ == "__main__":
    fig_lockscope()
    fig_rls()
    fig_helpers()
    scope = json.load(open(os.path.join(RES, "exp3_scope_history.json")))
    fig_scope([(p["label"], p["old_scope"], p["new_scope"]) for p in scope])
    print("figures written to", OUT)
