#!/usr/bin/env python3
"""COMPLEXA C271-C280: source-derived, deterministic, standard-library figures.

Usage:
    python make_figures.py --out content/supplementary
    python make_figures.py --check content/supplementary

Data are exact evaluations of explicit didactic models, not empirical samples.
SVG is generated directly so graphical elements remain vector and searchable.
"""
from __future__ import annotations
import argparse
import base64
import csv
from hashlib import sha256
from html import escape
from io import BytesIO, StringIO
from math import exp, log, sqrt, isclose
from xml.etree import ElementTree
from pathlib import Path
import zipfile

ROOT = Path(__file__).resolve().parents[2]
NAMES = {
    271: ("La ventaja cambia con la composición", "Pagos del juego de conflicto H–D; encuentro aleatorio"),
    272: ("Del diferencial de aptitud a la trayectoria", "Replicador continuo; ventaja constante y dependiente de frecuencia"),
    273: ("Cuatro problemas estratégicos distintos", "Diferencia de pago por cooperar frente a abstenerse"),
    274: ("La invasión prueba estabilidad evolutiva", "Resistencia de una mezcla H–D de probabilidad un medio"),
    275: ("La asociación cambia la selección", "Donación: beneficio 3, costo 1 y frecuencia inicial de C igual a 0,2"),
    276: ("Cooperación repetida y umbral de arranque", "Disparador permanente G frente a D; encuentros aleatorios"),
    277: ("Corregir errores cambia el resultado", "Cooperación estacionaria en parejas de estrategias idénticas"),
    278: ("Contribuir depende de ser decisivo", "Bien público con umbral de tres aportes entre cinco"),
    279: ("Dos escalas, dos signos de selección", "Ecuación de Price para dos grupos sintéticos de diez individuos"),
    280: ("Cooperación similar, mecanismos diferentes", "Asociación, relaciones repetidas e institución; frecuencia inicial 0,8"),
}
NAVY="#183448"; TEAL="#087F87"; OCHRE="#C56D35"; PURPLE="#7465AD"
INK="#263B4A"; MUTED="#66808B"; GRID="#E2EAEF"; BG="#F5F8FA"; GREEN="#4A9870"


def v(x):
    if isinstance(x,str): return x
    if isinstance(x,int): return str(x)
    return f"{x:.10f}"


def csv_bytes(header, rows):
    f=StringIO(newline="")
    w=csv.writer(f,lineterminator="\n")
    w.writerow(header)
    for row in rows: w.writerow([v(x) for x in row])
    return f.getvalue().encode("utf-8")


def X(text): return escape(str(text),quote=True)


class SVG:
    def __init__(self,n,title,desc):
        self.n=n; self.a=[]
        self.a.append(f'<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="760" viewBox="0 0 1200 760" role="img" aria-labelledby="ttl{n} des{n}">')
        self.a.append(f'<title id="ttl{n}">Figura {n}.1. {X(title)}</title>')
        self.a.append(f'<desc id="des{n}">{X(desc)}. Valores de modelos definidos en el manuscrito, no resultados observacionales.</desc>')
        self.rect(0,0,1200,760,"#FFFFFF")
        self.rect(0,0,13,760,TEAL)
        self.rect(43,28,1114,99,BG,15)
        self.text(65,56,"C O M P L E X A   /   LABORATORIO REPRODUCIBLE",13,TEAL,700)
        self.text(65,93,title,29,NAVY,700)
        self.text(1133,62,f"C{n}",24,NAVY,700,anchor="end")
        self.text(68,150,desc,17,MUTED,400)
        self.line(55,691,1153,691,GRID,2)
        self.text(64,718,"MODELO MATEMÁTICO / DATOS DIDÁCTICOS",12,MUTED,700)
        self.text(1138,718,f"complexa.science  ·  {n}.1",13,MUTED,500,anchor="end")
    def rect(self,x,y,w,h,color,r=0,stroke=None,sw=1):
        self.a.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{r}" fill="{color}"'+(f' stroke="{stroke}" stroke-width="{sw}"' if stroke else "")+'/>')
    def text(self,x,y,s,size=16,color=INK,weight=400,anchor="start",rotate=None):
        rot=f' transform="rotate({rotate} {x} {y})"' if rotate is not None else ""
        self.a.append(f'<text x="{x}" y="{y}" text-anchor="{anchor}" fill="{color}" font-family="DejaVu Sans, Arial, sans-serif" font-size="{size}" font-weight="{weight}"{rot}>{X(s)}</text>')
    def line(self,x1,y1,x2,y2,color,sw=1,dash=None):
        self.a.append(f'<line x1="{x1:.2f}" y1="{y1:.2f}" x2="{x2:.2f}" y2="{y2:.2f}" stroke="{color}" stroke-width="{sw}" stroke-linecap="round"'+(f' stroke-dasharray="{dash}"' if dash else "")+'/>')
    def circle(self,x,y,r,color,stroke=None):
        self.a.append(f'<circle cx="{x:.2f}" cy="{y:.2f}" r="{r}" fill="{color}"'+(f' stroke="{stroke}"' if stroke else "")+'/>')
    def path(self,p,color,sw=3,fill="none",dash=None):
        self.a.append(f'<path d="{p}" stroke="{color}" stroke-width="{sw}" stroke-linejoin="round" stroke-linecap="round" fill="{fill}"'+(f' stroke-dasharray="{dash}"' if dash else "")+'/>')
    def note(self,s):
        self.rect(65,641,1070,33,BG,8)
        self.text(80,663,s,14,INK,500)
    def svg(self):return ("\n".join(self.a)+"\n</svg>\n").encode("utf-8")


def plot(svg,area,xmin,xmax,ymin,ymax,xticks,yticks,xlabel,ylabel,series,tag=None,highlight=None,legend=True):
    """area=(x,y,w,h); series list (label,color,[(x,y)...])."""
    x,y,w,h=area
    svg.rect(x,y,w,h,"#FFFFFF",12,GRID)
    svg.text(x+20,y+33,tag or "MODELO Y COMPARACIÓN",17,NAVY,700)
    left=x+72; right=x+w-25; top=y+65; bottom=y+h-64
    def xx(t):return left+(t-xmin)*(right-left)/(xmax-xmin)
    def yy(t):return bottom-(t-ymin)*(bottom-top)/(ymax-ymin)
    for val in yticks:
        cy=yy(val);svg.line(left,cy,right,cy,GRID,1)
        label=f"{val:.2f}" if abs(val)<2 and val!=int(val) else f"{val:g}"
        svg.text(left-12,cy+5,label,12,MUTED,400,anchor="end")
    for val in xticks:
        cx=xx(val);svg.line(cx,bottom,cx,bottom+5,MUTED,1.1)
        label=f"{val:.2f}" if abs(val)<2 and val!=int(val) else f"{val:g}"
        svg.text(cx,bottom+23,label,12,MUTED,400,anchor="middle")
    svg.line(left,bottom,right,bottom,NAVY,1.7);svg.line(left,top,left,bottom,NAVY,1.7)
    svg.text((left+right)/2,y+h-17,xlabel,14,MUTED,500,anchor="middle")
    svg.text(x+23,(top+bottom)/2,ylabel,13,MUTED,500,anchor="middle",rotate=-90)
    for lab,color,vals in series:
        points=[(xx(a),yy(b)) for a,b in vals]
        d=" ".join(("M" if j==0 else "L")+f"{a:.2f},{b:.2f}" for j,(a,b) in enumerate(points))
        svg.path(d,color,3.8)
    if highlight:
        for item in highlight:
            hx,hy,itemtext,color=item
            cx=xx(hx);cy=yy(hy)
            svg.circle(cx,cy,5.3,"#FFFFFF",stroke=color)
            svg.circle(cx,cy,2.4,color)
            svg.text(cx+8,cy-10,itemtext,12,color,700)
    if legend:
        # Draw compact legend below panel heading, above plot.
        lx=x+w-22
        for label,color,vals in reversed(series):
            length=min(280,len(label)*8.3+28);lx-=length
            svg.line(lx,y+36,lx+18,y+36,color,3)
            svg.text(lx+24,y+41,label,13,INK,600)
            lx-=14
    return xx,yy


def bar_panel(svg,area,title,bars,ymax,unit="puntos",baseline=0):
    x,y,w,h=area
    svg.rect(x,y,w,h,"#FFFFFF",12,GRID)
    svg.text(x+20,y+33,title,17,NAVY,700)
    x0=x+70; x1=x+w-25; y0=y+66; y1=y+h-64
    def scale(z):return y1-(z-baseline)*(y1-y0)/(ymax-baseline)
    for q in range(5):
        z=baseline+(ymax-baseline)*q/4
        cy=scale(z);svg.line(x0,cy,x1,cy,GRID,1)
        svg.text(x0-9,cy+4,f"{z:.2f}" if ymax<3 else f"{z:.1f}",12,MUTED,400,"end")
    count=len(bars);block=(x1-x0)/count
    for i,(label,val,color) in enumerate(bars):
        bw=min(67,block*.61);bx=x0+i*block+block/2-bw/2
        by=scale(val);svg.rect(bx,by,bw,y1-by,color,4)
        svg.text(bx+bw/2,by-10,f"{val:.3f}" if ymax<=3 else f"{val:.2f}",13,color,700,anchor="middle")
        svg.text(bx+bw/2,y1+26,label,12,INK,600,anchor="middle")
    svg.text((x0+x1)/2,y+h-17,unit,13,MUTED,500,anchor="middle")


def make_data():
    D={}
    rows=[]
    for i in range(101):
        x=i/100;h=2-3*x; d=1-x
        wh=h+2;wd=d+2; nxt=x*wh/(x*wh+(1-x)*wd)
        rows.append((x,h,d,x*h+(1-x)*d,wh,wd,nxt))
    D[271]=(["frequency_H","payoff_H","payoff_D","population_mean_payoff","fitness_H","fitness_D","next_frequency_H"],rows)
    rows=[]
    for i in range(121):
        t=i/10;x=1/(1+9*exp(-.5*t));u=i/120
        rows.append((t,x,u,u*(1-u)*(1-2*u)))
    D[272]=(["time","constant_advantage_frequency","frequency_grid","conflict_replicator_field"],rows)
    rows=[]
    for i in range(101):
        x=i/100;rows.append((x,-1,1,1-2*x,4*x-3))
    D[273]=(["frequency_cooperators","donation_payoff_difference","cooperation_dominant_difference","snowdrift_difference","coordination_difference"],rows)
    rows=[]
    for i in range(101):
        q=i/100;d=2*(q-.5)**2;rows.append((q,d,.1*d))
    D[274]=(["mutant_probability_H","resident_second_condition_advantage","actual_advantage_at_epsilon_0_1"],rows)
    rows=[]
    for i in range(101):
        r=i/100;x=.2;b=3;c=1
        pc=r+(1-r)*x;pd=(1-r)*x
        pC=b*pc-c;pD=b*pd;wC=2+pC;wD=2+pD
        freq=x*wC/(x*wC+(1-x)*wD)
        rows.append((r,pc,pd,pC,pD,pC-pD,freq))
    D[275]=(["association_r","P_C_given_C","P_C_given_D","payoff_C","payoff_D","payoff_advantage_C","next_cooperator_frequency"],rows)
    rows=[]
    for i in range(101):
        x=i/100
        rows.append((x,2*.25*x-(1-.25),2*.5*x-(1-.5),2*.75*x-(1-.75)))
    D[276]=(["frequency_G","advantage_delta_0_25","advantage_delta_0_50","advantage_delta_0_75"],rows)
    rows=[]
    for i in range(101):
        e=.001+.199*i/100
        w=1-3*e+6*e*e-4*e**3
        cc=1-4*e+7*e*e-4*e**3
        rows.append((e,.5,w,1,2*w,cc))
    D[277]=(["execution_error","tft_stationary_cooperation","wsls_stationary_cooperation","tft_mean_payoff","wsls_mean_payoff","wsls_mutual_cooperation"],rows)
    rows=[]
    for i in range(101):
        p=i/100;pv=6*p*p*(1-p)*(1-p)
        rows.append((p,pv,3*pv-1))
    D[278]=(["probability_each_other_contributes","probability_exactly_two_of_four","advantage_of_contributing"],rows)
    rows=[
        ("Grupo A",.8,25.6/34,34,25.6),
        ("Grupo B",.2,1.6/16,16,1.6),
        ("Global",.5,.544,50,27.2),
        ("Entre grupos",.0,.108,0,.27),
        ("Dentro grupos",.0,-.064,0,-.16),
        ("Cambio neto",.5,.044,0,.11),
    ]
    D[279]=(["component","initial_frequency_or_zero","final_frequency_or_delta","offspring_total","cooperative_offspring_or_covariance"],rows)
    rows=[]
    for i in range(101):
        x=i/100
        rows.append((x,.5,1.5*x-.25,.5,2*x,1.5*x*x+.5*x,3.5*x-1.7))
    D[280]=(["frequency","advantage_association","advantage_repeated_game","advantage_institution","mean_association","mean_repeated_game","mean_institution"],rows)
    return D


def charts(n,data):
    title,subtitle=NAMES[n]
    s=SVG(n,title,subtitle)
    if n==271:
        plot(s,(55,176,1090,450),0,1,-1.1,2.1,[0,.25,.5,.75,1],[-1,0,.5,1,1.5,2],"Frecuencia de H en la población","Pago esperado",[
        ("H",OCHRE,[(r[0],r[1]) for r in data]),("D",TEAL,[(r[0],r[2]) for r in data]),("Promedio",PURPLE,[(r[0],r[3]) for r in data])],"VENTAJA CUANDO H ES RARA",[(.5,.5,"H = D",NAVY)])
        s.note("x = 0,25: H recibe 1,25; D recibe 0,75. La ventaja se invierte al superar x = 0,5.")
    elif n==272:
        plot(s,(55,176,545,450),0,12,0,1,[0,3,6,9,12],[0,.25,.5,.75,1],"Tiempo (unidades)","Frecuencia",[("x(t)",TEAL,[(r[0],r[1]) for r in data])],"VENTAJA CONSTANTE: k = 0,5",[(2*log(9),.5,"x = 0,5",TEAL)])
        plot(s,(614,176,531,450),0,1,-.1,.1,[0,.25,.5,.75,1],[-.1,-.05,0,.05,.1],"Frecuencia x","Variación por tiempo",[("dx/dt",PURPLE,[(r[2],r[3]) for r in data])],"CONFLICTO: EQUILIBRIO ESTABLE",[(.5,0,"x* = 0,5",PURPLE)])
        s.note("La misma ecuación del replicador admite trayectorias crecientes o retorno al equilibrio interior.")
    elif n==273:
        plot(s,(55,176,1090,450),0,1,-3.2,1.2,[0,.25,.5,.75,1],[-3,-2,-1,0,1],"Frecuencia de cooperadores x","Ventaja de cooperar",[
            ("Donación",OCHRE,[(r[0],r[1]) for r in data]),
            ("C dominante",TEAL,[(r[0],r[2]) for r in data]),
            ("Coexistencia",PURPLE,[(r[0],r[3]) for r in data]),
            ("Coordinación",GREEN,[(r[0],r[4]) for r in data])],"CUATRO MATRICES, CUATRO CAMPOS SELECTIVOS",[(.5,0,"x = 1/2",PURPLE),(.75,0,"x = 3/4",GREEN)])
        s.note("Valores positivos favorecen C; valores negativos favorecen D. Los umbrales dependen de la matriz.")
    elif n==274:
        plot(s,(55,176,1090,450),0,1,0,.53,[0,.25,.5,.75,1],[0,.1,.2,.3,.4,.5],"Probabilidad H del mutante q","Ventaja del residente",[
        ("Condición ESS",TEAL,[(r[0],r[1]) for r in data]),("ε = 0,1",OCHRE,[(r[0],r[2]) for r in data])],"MEZCLA RESIDENTE: p(H) = 0,5",[(.5,0,"residentes = mutante",NAVY)])
        s.note("La ganancia ESS es estricta para q distinto de 0,5; la invasión al 10 % escala esa ventaja.")
    elif n==275:
        plot(s,(55,176,545,450),0,1,-1.05,2.05,[0,.25,.5,.75,1],[-1,0,1,2],"Asociación r","Pago C menos pago D",[("3r − 1",OCHRE,[(r[0],r[5]) for r in data])],"UMBRAL DE INVASIÓN",[(1/3,0,"r = 1/3",OCHRE)])
        plot(s,(614,176,531,450),0,1,.15,.3,[0,.25,.5,.75,1],[.15,.2,.25,.3],"Asociación r","Frecuencia siguiente",[("x'",TEAL,[(r[0],r[6]) for r in data]),("Inicial",PURPLE,[(0,.2),(1,.2)])],"REPRODUCCIÓN DESDE x = 0,2",[(.5,7/30,"7/30",TEAL)])
        s.note("La productividad total es igual en ambos escenarios; cambia la distribución de los encuentros.")
    elif n==276:
        plot(s,(55,176,1090,450),0,1,-.8,1.8,[0,.25,.5,.75,1],[-.75,-.5,0,.5,1,1.5],"Frecuencia de disparador permanente G","Pago G menos pago D",[
        ("δ = 0,25",OCHRE,[(r[0],r[1]) for r in data]),
        ("δ = 0,50",PURPLE,[(r[0],r[2]) for r in data]),
        ("δ = 0,75",TEAL,[(r[0],r[3]) for r in data])],"EL UMBRAL DE ARRANQUE DEPENDE DE LA CONTINUIDAD",[(1/6,0,"δ = 0,75: x* = 1/6",TEAL)])
        s.note("Con δ = 0,25 no hay crecimiento de G desde ninguna frecuencia interior de este juego.")
    elif n==277:
        plot(s,(55,176,1090,450),0,.2,.35,1.02,[0,.05,.10,.15,.20],[.4,.5,.6,.7,.8,.9,1],"Probabilidad de error de ejecución e","Cooperación estacionaria",[
            ("Tit-for-tat",OCHRE,[(r[0],r[1]) for r in data]),
            ("Win-stay, lose-shift",TEAL,[(r[0],r[2]) for r in data])],"PAREJAS HOMOGÉNEAS Y ERROR POSITIVO",[(.01,1-3*.01+6*.01**2-4*.01**3,"0,970596",TEAL)])
        s.note("El límite e → 0 del régimen estacionario difiere de fijar e = 0 al comenzar en CC.")
    elif n==278:
        plot(s,(55,176,1090,450),0,1,-1.05,.20,[0,.25,.5,.75,1],[-1,-.75,-.5,-.25,0,.125],"Probabilidad p de que otro contribuya","Ventaja de contribuir",[
            ("Δ(p)",TEAL,[(r[0],r[2]) for r in data])],"BENEFICIO DE SER DECISIVO EN PROYECTO UMBRAL",[(.5,.125,"p = 0,5: Δ = 1/8",TEAL)])
        s.note("El proyecto requiere tres contribuciones de cinco; la ventaja vuelve a ser negativa fuera de la zona intermedia.")
    elif n==279:
        # Signed Price components: bars originate at zero, not at the plot minimum.
        x,y,w,h=(55,176,545,450)
        s.rect(x,y,w,h,"#FFFFFF",12,GRID)
        s.text(x+20,y+33,"COMPONENTES DEL CAMBIO GLOBAL",17,NAVY,700)
        l=x+78;rr=x+w-24;top=y+80;bot=y+h-69
        ymin=-.085;ymax=.125
        def sy(z):return bot-(z-ymin)/(ymax-ymin)*(bot-top)
        for z in [-.08,-.04,0,.04,.08,.12]:
            yy=sy(z);s.line(l,yy,rr,yy,GRID if z else NAVY,1.7 if z==0 else 1)
            s.text(l-12,yy+5,f"{z:+.2f}" if z else "0",12,MUTED,400,"end")
        B=[("Entre",.108,TEAL),("Dentro",-.064,OCHRE),("Neto",.044,PURPLE)]
        step=(rr-l)/len(B)
        for i,(lab,val,col) in enumerate(B):
            bw=70;cx=l+step*(i+.5)
            yv=sy(val);yz=sy(0)
            s.rect(cx-bw/2,min(yv,yz),bw,abs(yv-yz),col,5)
            s.text(cx,yv-12 if val>=0 else yv+23,f"{val:+.3f}",14,col,700,"middle")
            s.text(cx,bot+28,lab,13,INK,600,"middle")
        s.text((l+rr)/2,y+h-17,"Cambio de frecuencia Δx",13,MUTED,500,"middle")
        # Paired initial/final frequencies for A, B and the whole population.
        x,y,w,h=(614,176,531,450)
        s.rect(x,y,w,h,"#FFFFFF",12,GRID)
        s.text(x+20,y+33,"DESCENSO LOCAL; AUMENTO GLOBAL",16,NAVY,700)
        l=x+66;rr=x+w-25;top=y+70;bot=y+h-69
        for q in [0,.2,.4,.6,.8,1]:
            yy=bot-q*(bot-top)
            s.line(l,yy,rr,yy,GRID)
            s.text(l-10,yy+5,f"{q:.1f}",12,MUTED,400,"end")
        cases=[("Grupo A",.8,25.6/34),("Grupo B",.2,.1),("Global",.5,.544)]
        step=(rr-l)/3
        for i,(lab,old,new) in enumerate(cases):
            cx=l+(i+.5)*step
            for dx,vv,col in [(-19,old,PURPLE),(19,new,TEAL)]:
                yy=bot-vv*(bot-top)
                s.rect(cx+dx-15,yy,30,bot-yy,col,3)
                s.text(cx+dx,yy-10,f"{vv:.3f}",12,col,700,"middle")
            s.text(cx,bot+29,lab,12,INK,600,"middle")
        s.line(x+27,y+57,x+47,y+57,PURPLE,5);s.text(x+53,y+62,"Antes",12,INK,600)
        s.line(x+125,y+57,x+145,y+57,TEAL,5);s.text(x+151,y+62,"Después",12,INK,600)
        s.text((l+rr)/2,y+h-17,"Frecuencia de cooperación",13,MUTED,500,"middle")
        s.note("La selección entre grupos (+0,108) supera la pérdida interna (−0,064): Δx = +0,044.")
    elif n==280:
        plot(s,(55,176,545,450),0,1,-.3,1.3,[0,.25,.5,.75,1],[-.25,0,.25,.5,.75,1,1.25],"Frecuencia cooperadora","Ventaja de la regla",[
            ("Asociación e inst.",TEAL,[(r[0],r[1]) for r in data]),
            ("Repetición",OCHRE,[(r[0],r[2]) for r in data])],"DIFERENCIA DE PAGOS",[(1/6,0,"x* = 1/6",OCHRE)])
        plot(s,(614,176,531,450),0,1,-1.9,2.2,[0,.25,.5,.75,1],[-1.5,-1,0,1,2],"Frecuencia cooperadora","Pago medio",[
            ("Asociación",TEAL,[(r[0],r[4]) for r in data]),
            ("Repetición",OCHRE,[(r[0],r[5]) for r in data]),
            ("Institución",PURPLE,[(r[0],r[6]) for r in data])],"PAGO MEDIO",[(.8,1.6,"1,6",TEAL),(.8,1.36,"1,36",OCHRE),(.8,1.1,"1,1",PURPLE)])
        s.note("Asociación e institución producen la misma trayectoria de x, pero distintos pagos colectivos.")
    return s.svg()


def assert_near(actual,expected,description):
    if not isclose(actual,expected,rel_tol=1e-8,abs_tol=1e-9):
        raise AssertionError(f"{description}: {actual} is not {expected}")


def audit_model(data):
    # Exact independent checks against the worked examples in each manuscript.
    t=data[271][1][25]
    assert_near(t[1],1.25,"C271 H payoff at x=1/4")
    assert_near(t[2],.75,"C271 D payoff at x=1/4")
    assert_near(t[6],13/46,"C271 next frequency")
    assert_near(1/(1+9*exp(-.5*2*log(9))),.5,"C272 passage at x=1/2")
    assert_near(data[272][1][30][3],.09375,"C272 frequency-dependent velocity")
    assert_near(data[273][1][50][3],0,"C273 Snowdrift equilibrium")
    assert_near(data[273][1][75][4],0,"C273 coordination threshold")
    assert_near(data[274][1][0][1],.5,"C274 ESS quadratic resistance")
    assert_near(data[274][1][0][2],.05,"C274 invasion at epsilon=0.1")
    assert_near(data[275][1][50][5],.5,"C275 association advantage")
    assert_near(data[275][1][50][6],7/30,"C275 next frequency at r=0.5")
    assert_near(data[275][1][20][6],13/75,"C275 next frequency at r=0.2")
    assert_near(2*.75*(1/6)-(1-.75),0,"C276 cooperation threshold")
    e=.01
    assert_near(1-3*e+6*e**2-4*e**3,.970596,"C277 WSLS cooperation")
    assert_near(1-4*e+7*e**2-4*e**3,.960696,"C277 mutual cooperation")
    assert_near(data[278][1][50][2],.125,"C278 pivotal contribution at p=0.5")
    assert_near(data[278][1][20][2],-.5392,"C278 outside the pivotal region")
    assert_near(.108-.064,.044,"C279 Price decomposition")
    assert_near(data[279][1][2][2],.544,"C279 global frequency")
    r=data[280][1][80]
    assert_near(r[4],1.6,"C280 association mean x=0.8")
    assert_near(r[5],1.36,"C280 repetition mean x=0.8")
    assert_near(r[6],1.1,"C280 institutional mean x=0.8")
    assert len(data)==10
    for n,(head,rows) in data.items():
        if not head or not rows or any(len(row)!=len(head) for row in rows):
            raise AssertionError(f"C{n}: invalid data shape")


def byte_outputs():
    data=make_data();audit_model(data);files={}
    for n in range(271,281):
        header,rows=data[n]
        files[f"repro/c271-280/c{n}.csv"]=csv_bytes(header,rows)
        fig=charts(n,rows)
        root=ElementTree.fromstring(fig)
        ns="{http://www.w3.org/2000/svg}"
        if root.find(ns+"title") is None or root.find(ns+"desc") is None:
            raise AssertionError(f"C{n}: missing SVG accessibility descriptions")
        files[f"figures/complexa-c{n}-fig01.svg"]=fig
    return files


def make_archive(files,readme,script):
    entries=dict(files)
    entries["repro/c271-280/README.md"]=readme
    entries["repro/c271-280/make_figures.py"]=script
    stream=BytesIO()
    with zipfile.ZipFile(stream,"w",compression=zipfile.ZIP_STORED,allowZip64=False) as archive:
        for name in sorted(entries):
            info=zipfile.ZipInfo(name,date_time=(2026,10,26,12,0,0))
            info.compress_type=zipfile.ZIP_STORED
            info.create_system=3
            info.external_attr=0o100644<<16
            archive.writestr(info,entries[name])
    return stream.getvalue()


def outputs():
    f=byte_outputs()
    here=Path(__file__)
    readme=here.with_name("README.md").read_bytes()
    script=here.read_bytes()
    archive=make_archive(f,readme,script)
    f["zip-data/complexa-c271-c280-reproducible.zip.b64"]=base64.b64encode(archive)+b"\n"
    return f,archive


def run():
    parser=argparse.ArgumentParser(description=__doc__)
    options=parser.add_mutually_exclusive_group()
    options.add_argument("--out",type=Path,help="Directory content/supplementary to write")
    options.add_argument("--check",type=Path,help="Directory to verify exact SHA of outputs")
    args=parser.parse_args()
    dest=args.out or args.check or ROOT
    f,archive=outputs()
    for relative,blob in sorted(f.items()):
        path=dest/relative
        if args.check:
            if not path.is_file() or path.read_bytes()!=blob:
                raise SystemExit(f"FAIL: reproducibility mismatch: {relative}")
        else:
            path.parent.mkdir(parents=True,exist_ok=True)
            path.write_bytes(blob)
    if len(f)!=21 or len(make_data())!=10:raise AssertionError("Unexpected set of supplements")
    with zipfile.ZipFile(BytesIO(archive)) as archive_file:
        if len(archive_file.namelist())!=22 or archive_file.testzip() is not None:
            raise AssertionError("Invalid 22-file ZIP")
    mode="CHECK" if args.check else "GENERATED"
    print(f"{mode}: 10 SVG, 10 CSV, deterministic ZIP; SHA-256 {sha256(archive).hexdigest()}")

if __name__=="__main__":run()
