"""Generate downloadable SVGs from the curated report dataset. No web calls."""
from pathlib import Path
from html import escape
import math,json
R=Path(__file__).resolve().parents[1]
OUT=R/'assets/graphics'
N='#091a40'; RED='#fe0942'; MUT='#596781'; LIGHT='#d8e0ef'; PAPER='#f3f5fa'
def text(x,y,s,size=13,color=N,weight='400',anchor='start'):
 return f'<text x="{x}" y="{y}" fill="{color}" font-size="{size}" font-weight="{weight}" text-anchor="{anchor}">{escape(str(s))}</text>'
def wrap_text(x,y,s,width=28,size=12,color=MUT):
 import textwrap
 return ''.join(text(x,y+i*18,line,size,color) for i,line in enumerate(textwrap.wrap(s,width)))
def rect(x,y,w,h,fill,rx=8,stroke='none'):
 return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{rx}" fill="{fill}" stroke="{stroke}"/>'
def line(x1,y1,x2,y2,c=LIGHT,w=1,dash=''):
 return f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{c}" stroke-width="{w}"'+(f' stroke-dasharray="{dash}"' if dash else '')+'/>'
def svg(name,title,body,w=680,h=350,desc=''):
 s=f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" role="img" aria-labelledby="title desc"><title id="title">{escape(title)}</title><desc id="desc">{escape(desc or title)}</desc><g font-family="Arial, sans-serif">{body}</g></svg>'
 (OUT/(name+'.svg')).write_text(s,encoding='utf-8');return name

def donut(name,values,labels,center,subtitle,colors=None):
 colors=colors or [N,RED,LIGHT];total=sum(values);body='';a=-90;cx=165;cy=170;radius=108
 for val,c in zip(values,colors):
  b=a+360*val/total;x1=cx+radius*math.cos(math.radians(a));y1=cy+radius*math.sin(math.radians(a));x2=cx+radius*math.cos(math.radians(b));y2=cy+radius*math.sin(math.radians(b))
  body+=f'<path d="M{x1},{y1} A{radius},{radius} 0 {int(b-a>180)} 1 {x2},{y2}" fill="none" stroke="{c}" stroke-width="28"/>';a=b
 body+=text(cx,173,center,45,N,'700','middle')+text(cx,200,subtitle,12,MUT,anchor='middle')
 for i,(v,l,c) in enumerate(zip(values,labels,colors)):
  y=97+i*75;body+=rect(340,y-13,10,10,c,3)+text(363,y,l,13)+text(363,y+25,str(v).replace('.',','),22,N,'700')
 return svg(name,' / '.join(labels),body,680,350,'; '.join(f'{a}: {b}' for a,b in zip(labels,values)))
def bars(name,title,rows,unit='',colors=None,caption='',height=None):
 h=height or (len(rows)*49+50);body='';mx=max(v for _,v in rows) or 1
 for i,(label,val) in enumerate(rows):
  y=20+i*49;c=(colors or [N]*len(rows))[i%len(colors or [N]*len(rows))];body+=text(2,y+12,label,12,N)+rect(232,y,360,16,PAPER,4)+rect(232,y,360*val/mx,16,c,4)+text(650,y+13,f'{val:g}'.replace('.',',')+unit,12,N,'700','end')
 if caption:body+=text(2,h-6,caption,10,MUT)
 return svg(name,title,body,680,h,'; '.join(f'{a}: {b}{unit}' for a,b in rows))
def pipeline(name,title,items,sub='Schematische Darstellung. Kein Laufzeitnachweis.'):
 body='';w=900;xs=[24+i*178 for i in range(len(items))]
 for i,(label,detail) in enumerate(items):
  x=xs[i]
  if i<len(items)-1:body+=line(x+152,90,x+175,90,N,2)+f'<path d="m{x+168} 85 7 5-7 5" stroke="{N}" fill="none" stroke-width="2"/>'
  body+=rect(x,25,154,128,N if i==len(items)-1 else PAPER,12)
  co='white' if i==len(items)-1 else N
  body+=text(x+15,51,f'0{i+1}',11,RED if co!= 'white' else '#ffafc1','700')+text(x+15,80,label,16,co,'700')+wrap_text(x+15,106,detail,21,10,'#c4cfe2' if co=='white' else MUT)
 body+=text(24,190,sub,11,MUT)
 return svg(name,title,body,w,212)
def build():
 OUT.mkdir(parents=True,exist_ok=True)
 d=json.loads((R/'data/modelle.json').read_text())
 donut('stunden',[9.5,25.5,5],['Frühere Vorleistung','September-Arbeit','Geschätzte Zuordnung'],'40 h','vorgegebener Rahmen',[LIGHT,N,RED])
 donut('one-shot-bilanz',[4,7,13],['Strenger verify-PASS','Marke ohne PASS-Beleg','Offen / unvollständig'],'24','historische Pakete',[N,RED,LIGHT])
 donut('ticketstatus',[228,78,18],['Offen','Status „Erledigt“','Andere Abschlussarten'],'324','Vorgänge · Aggregat',[RED,N,LIGHT])
 bars('zeitfenster','Aufruf-Zeitfenster, nicht Arbeitsstunden',[('Summe Einzelintervalle',38),('Ohne Überlappung',16.4)],' h',[N,RED],caption='Enthält Pausen. Keine Menschen-, CPU- oder GPU-Stunden.')
 bars('teststand','Historischer Testumfang',[('Bestanden',29),('Optional übersprungen',1)],'', [N,LIGHT],caption='Historischer Nachweis. Odoo-ORM separat blockiert.')
 bars('anforderungsgate','Historische Operator-Prüfung',[('Belegt',11),('Teilweise',2)],'', [N,RED],caption='13 Operator-Anweisungen; nicht die sechs Kundenanforderungen.')
 bars('schema-module','15 abgeleitete Fachgruppen',[(a,b) for a,b in d['schema_buckets']],colors=[N if 'Zeit' not in a else RED for a,b in d['schema_buckets']],caption='Zuordnung aus dem Schema abgeleitet; kein Nachweis aller Geschäftsregeln.')
 bars('ticketjahre','Vorgänge nach Entstehungsjahr',[('2022',70),('2023',155),('2024',59),('2025',33),('2026 bis 21. Juli',7)],colors=[LIGHT,N,LIGHT,LIGHT,RED],caption='Nur die Ideensammlung. Kein Gesamtbild des Supports.')
 bars('mindestloehne','Kollektivvertragliche Mindeststundenlöhne',[(f'Lohngruppe {a}',b) for a,b in zip(['I','II','III','IV','V','VI','VII'],[17.39,16.72,15.99,15.17,14.55,14.45,14.14])],' €',[N]*7,caption='Tischler · brutto · 01.05.2026–30.04.2027 · WKO P02')
 bars('lehrlinge','Lehrlinge im Jahresvergleich',[('2023',2267),('2024',2144),('2025',1987)],colors=[LIGHT,N,RED],caption='WKO-Statistik P01. Kein Beleg für eine Ursache der Veränderung.')
 pipeline('fachablauf','Vom Erfassungsereignis zur Buchung',[('Erfassen','Handy, Büro, Scanner'),('Rohzeit','Originaleingang bleibt'),('Prüfen','Zuordnung und Fehler'),('Freigeben','Verantwortliche Rolle'),('Buchen','Nachvollziehbarer Stand')])
 pipeline('bauablauf','Wie der Arbeitsstand entstand',[('Klären','Bedarf und Unterlagen'),('Aufteilen','Regeln und Module'),('Bauen','Odoo und eigener Weg'),('Prüfen','Tests und Gegenprüfung'),('Übergeben','Offene Punkte sichtbar')])
 pipeline('quellenweg','Quellen zu belastbaren Aussagen',[('Sammeln','Nur erlaubte Quellen'),('Trennen','Original und Entwurf'),('Abgleichen','Widersprüche klären'),('Redigieren','Identitäten entfernen'),('Belegen','Quelle plus Grenze')])
 pipeline('privacy-weg','Prüfung des Ausgabepakets',[('Auswählen','Datei-Allowlist'),('Redigieren','Nur Textkopien'),('Scannen','Text und PDF-Inhalt'),('Bildprüfung','Pixelsicht / Metadaten'),('Paket','Entpackt neu prüfen')],sub='Technische Prüfung, keine Garantie vollständiger Anonymität oder Rechtsfreigabe.')
 # Two equal alternatives, no product quality ranking.
 b=rect(245,8,250,56,N,12)+text(370,43,'Lunis Ist-Zeit · Fachkern',18,'white','700','middle')
 b+=line(370,64,370,89,N,2)+line(175,89,570,89,N,2)+line(175,89,175,111,N,2)+line(570,89,570,111,N,2)
 for x,title,lines in [(20,'Odoo 18 Community',['7 Fachmodule','Odoo-Oberflächen und Datenmodell','Integration neu prüfen']), (415,'Eigenständiger Weg',['FastAPI / SQLite / Weboberfläche','Rohzeiten, Prüfung, Buchung','Mehrbenutzerbetrieb neu prüfen'])]:
  b+=rect(x,112,315,183,PAPER,14)+text(x+20,148,title,21,N,'700')
  for i,l in enumerate(lines):b+=text(x+20,180+i*29,l,12,MUT)
 b+=text(20,330,'Odoo 19: dokumentiertes Zielbild, nicht als kompatibel bestätigt.',12,MUT)
 svg('architektur','Zwei technische Wege für denselben Fachkern',b,750,350)
 # Historical observation windows in UTC, not a continuous active-time series.
 waves=[('Wave 0',2+8/60,3+26/60),('Wave 1',3+36/60,4+34/60),('Integration',4+38/60,6+11/60),('Wave 2 / Fix',6+15/60,7+55/60),('Wave 3a',8+59/60,11+17/60),('Proben / Wave 4',11+35/60,20+12/60)]
 b='';start=2;end=21;x0=135;scale=480/(end-start)
 for hr in [2,6,10,14,18,21]:b+=line(x0+(hr-start)*scale,23,x0+(hr-start)*scale,287,LIGHT,1)+text(x0+(hr-start)*scale,15,f'{hr:02}:00',10,MUT,anchor='middle')
 for i,(title,a,z) in enumerate(waves):
  y=39+i*41;b+=text(4,y+13,title,12,N)+rect(x0+(a-start)*scale,y,(z-a)*scale,18,RED if i==5 else N,4)
 b+=text(4,315,'UTC am 20.09.2026 · erster bis letzter beobachteter Aufruf je Phase',11,MUT)+text(4,337,'Bridge-Pause 13:49–19:20 liegt im letzten Fenster. Keine aktive Arbeitszeit.',11,MUT)
 svg('wellen','Historische Ausführungsfenster',b,660,360)
 # Cost model bounds; no claim of equal final scope or measured speed factor.
 rows=d['classic_effort_model']['rows'];b=''
 for i,(title,lo,hi) in enumerate(rows):
  y=20+i*45;b+=text(0,y+12,title,11,N)+rect(255,y,330*hi/120,16,PAPER,4)+rect(255,y,330*lo/120,16,N,4)+text(665,y+13,f'{lo}–{hi} h',11,N,'700','end')
 b+=text(0,308,'Modell M01: 224–432 h. Kein Marktangebot und kein Gleichleistungs-Benchmark.',10,MUT)
 svg('klassisch-modell','Annahme für eine klassische Entwicklung',b,680,329)
 # ROI default and two sensitivities, all openly marked assumptions.
 b='';x0=57;y0=274;w=560;h=232;lo=-6000;hi=30000
 def xy(m,val):return x0+m/24*w,y0-(val-lo)/(hi-lo)*h
 for val in [-6000,0,15000,30000]:
  _,y=xy(0,val);b+=line(x0,y,x0+w,y,LIGHT)+text(x0-9,y+4,f'{val/1000:g}k €',10,MUT,anchor='end')
 for m in [0,6,12,18,24]:x,_=xy(m,0);b+=text(x,298,str(m),10,MUT,anchor='middle')
 for monthly,col in [(25*minutes/60*20*.75*35-150,col) for minutes,col in [(3,LIGHT),(5,RED),(7,N)]]:
  pts=[xy(m,-6000+m*monthly) for m in range(25)];b+=f'<polyline points="'+ ' '.join(f'{x:.1f},{y:.1f}' for x,y in pts)+f'" fill="none" stroke="{col}" stroke-width="3"/>'
 b+=text(57,324,'Monate · Kapazitätswert minus Modellkosten · keine Gewinnprognose',11,MUT)
 svg('roi-modell','ROI-Beispiel mit 3, 5 oder 7 Minuten Zeitgewinn',b,680,345,'Drei Annahmen: 3, 5 oder 7 Minuten je Person und Tag. 25 Personen, 20 Tage, 75 Prozent Nutzungsgrad, 35 Euro Bewertung, 150 Euro Betrieb und 6000 Euro Startwert. Die dargestellten Monatswerte werden vom Datenmodell nachgerechnet.')
 # Release steps list, deliberately not a percentage complete.
 b=''
 for i,(a,z) in enumerate([('Version','18 oder Ziel 19 klären'),('Sicherheit','Daten und Geheimnisse prüfen'),('Fachfälle','Geräte bis Buchung testen'),('Betrieb','Backup und Restore prüfen'),('Freigabe','Pilot bewerten und entscheiden')]):
  y=10+i*59;b+=rect(0,y,45,42,N if i<2 else PAPER,9)+text(22,y+28,str(i+1),17,'white' if i<2 else N,'700','middle')+text(62,y+15,a,14,N,'700')+text(62,y+34,z,12,MUT)
 svg('freigabeweg','Fünf Prüfschritte vor produktiver Freigabe',b,600,315)
 return [p.name for p in OUT.glob('*.svg')]
if __name__=='__main__':print('Generated',len(build()),'SVG graphics')
