#!/usr/bin/env python3
"""Offline Meplar chart reproduction; stdlib + installed librsvg. No network."""
import csv
import hashlib
import json
import re
import subprocess
import sys
from decimal import Decimal
from pathlib import Path
from common import SVG, INK, MUTED, TEAL, RUST, GRID, PAPER, one, signed

ROOT = Path(__file__).resolve().parent
FIFA = 'fifa-2026-goals-and-rates'
ACI = 'international-airports-2025-recovery'

def write_json(path,data):
    path.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n')

def csv_write(path,rows):
    with path.open('w',newline='') as f:
        w=csv.DictWriter(f,list(rows[0]),lineterminator='\n');w.writeheader();w.writerows(rows)

def fifa_data():
    source=json.loads((ROOT/'raw/fifa-golden-boot-dom-extract.json').read_text())
    all_rows=source['rows']
    assert [int(r['fifa_rank']) for r in all_rows]==list(range(1,12))
    assert all(all_rows[i]['goals']>=all_rows[i+1]['goals'] for i in range(10))
    assert all_rows[10]['goals']==3
    display=['Mbappé','Messi','Bellingham','Haaland','Dembélé','Kane','Oyarzabal','Sarr','Quiñones','Vinícius Júnior']
    rows=[]
    for r,name in zip([r for r in all_rows if r['goals']>=4],display):
        rate=Decimal(r['goals'])*90/Decimal(r['minutes_played'])
        rows.append({**r,'display_name':name,'goals_per_90':rate,'goals_per_90_display':one(rate)})
    assert len(rows)==10
    assert abs(rows[0]['goals_per_90']-rows[3]['goals_per_90'])<Decimal('.003')
    assert rows[0]['goals_per_90_display']==rows[3]['goals_per_90_display']=='1.2'
    d=ROOT/FIFA;d.mkdir(exist_ok=True)
    csv_write(d/'chart-data.csv',rows)
    csv_write(d/'source-table-with-cohort-boundary.csv',all_rows)
    write_json(d/'metadata.json',{
        'status':'Unpublished candidate', 'geography':'FIFA Men’s World Cup 2026 tournament',
        'cohort':'All ten players with four or more goals in the official Golden Boot table. Rank 11 has three. All Teams selected. Not a tournament-wide per-90 leaderboard.',
        'source_url':source['source_url'],'retrieved':'2026-10-22','final_context':source['final_context'],
        'capture_type':source['capture_type'],'source_artifact':'raw/fifa-golden-boot-dom-extract.json',
        'formula':'90 × match goals / FIFA Minutes Played, using integer inputs from one consistent official table',
        'penalties':'Match penalties retained. No non-penalty adjustment or shootout goals added.',
        'minutes':'FIFA total active participation; source glossary verified in browser. No editorial minute totals substituted.',
        'rounding':'Rate labels show one decimal; CSV preserves full Decimal calculation. Similar displayed values can conceal small numerical differences.',
        'layout':'Two separately labelled zero-based scales: goals 0–10; goals per 90 0–1.2. Same FIFA scorer order in both panels. Minutes are shown beside each player.',
        'limits':'Descriptive rates for a goal-selected cohort, not causal efficiency, player quality or equal-strength opposition. Minutes and opportunities differ.'})
    return rows

def fifa_chart(rows,mobile=False):
    w,h=(400,1650) if mobile else (1200,1550)
    s=SVG(w,h,'Mbappé and Haaland: more goals, almost the same rate',
          'FIFA World Cup 2026. All ten players with four or more goals, in FIFA scorer order. Mbappé: 10 goals in 769 minutes; Haaland: 7 in 537. Both round to 1.2 goals per 90. Formula 90 times goals divided by minutes. Penalties retained. Two separate zero-based scales; this is not a player-quality ranking.')
    if mobile:
        s.text(24,36,'MEPLAR  /  WORLD CUP 2026',18,MUTED,700)
        s.lines(24,88,['More goals.','Almost the same rate.'],31,38,weight=700)
        s.text(24,175,'Mbappé and Haaland · Men’s World Cup',18,MUTED)
        s.lines(24,222,['10 and 7 goals; both 1.2 per 90.', 'All ten players with 4+ goals shown.'],18,27)
        s.text(24,305,'Player / minutes',18,MUTED,700)
        s.text(212,305,'Goals',18,MUTED,700,anchor='middle')
        s.text(334,305,'Per 90',18,MUTED,700,anchor='middle')
        g0,g1,r0,r1,start,step=174,244,294,350,380,92
        for x,v in [(g0,'0'),(g1,'10'),(r0,'0'),(r1,'1.2')]:s.text(x,337,v,18,MUTED,anchor='middle')
    else:
        s.text(64,50,'MEPLAR  /  FIFA MEN’S WORLD CUP 2026',20,MUTED,700)
        s.lines(64,128,['More goals.','Almost the same rate.'],62,68,weight=700)
        s.text(64,256,'Mbappé and Haaland · all ten players with four or more goals',27,MUTED)
        s.text(64,329,'One FIFA table for goals and minutes · match penalties included',24,MUTED)
        s.text(64,405,'Player / minutes played',23,MUTED,700)
        s.text(495,405,'Goals',26,MUTED,700,anchor='middle')
        s.text(935,405,'Goals per 90',26,MUTED,700,anchor='middle')
        g0,g1,r0,r1,start,step=350,630,800,1070,498,76
        for x,v in [(g0,'0'),((g0+g1)/2,'5'),(g1,'10'),(r0,'0'),((r0+r1)/2,'0.6'),(r1,'1.2')]:s.text(x,448,v,21,MUTED,anchor='middle')
    for idx,r in enumerate(rows):
        y=start+idx*step
        accent=r['source_player_name'] in ['Kylian Mbappe','Erling Haaland']
        color=TEAL if accent else INK
        if accent:s.rect(16 if mobile else 48,y-29,w-(32 if mobile else 96),step-5,'#e3efec',6)
        s.text(24 if mobile else 64,y,r['display_name'],18 if mobile else 27,color,700 if accent else 400)
        s.text(24 if mobile else 64,y+26,f'{r["minutes_played"]} min',18 if mobile else 21,MUTED)
        yy=y+6
        for x0,x1,key,maxval in [(g0,g1,'goals',10),(r0,r1,'goals_per_90',1.2)]:
            s.line(x0,yy,x1,yy,GRID,1)
            x=x0+float(r[key])/maxval*(x1-x0)
            s.line(x0,yy,x,yy,color,11 if mobile else 16,extra=f'data-player="{r["fifa_rank"]}" data-metric="{key}" data-value="{r[key]}"')
        s.text(270 if mobile else 697,y+12,r['goals'],18 if mobile else 26,color,700,anchor='end')
        s.text(382 if mobile else 1136,y+12,r['goals_per_90_display'],18 if mobile else 26,color,700,anchor='end')
    if mobile:
        s.line(24,1290,376,1290)
        s.lines(24,1330,['Goals per 90 = 90 × goals ÷ minutes.', 'Rate labels rounded to one decimal.', 'Match penalties are included.'],18,27)
        s.lines(24,1441,['Descriptive rates; no adjustment for', 'opponents, role or playing conditions.', 'The cohort is selected by goal totals.'],18,27,color=MUTED)
        s.lines(24,1560,['Source: FIFA · Golden Boot table', 'Snapshot: 22 Oct 2026 · meplar.com'],18,27,color=MUTED)
    else:
        s.line(64,1278,1136,1278)
        s.lines(64,1324,['Goals per 90 = 90 × goals ÷ FIFA minutes. Rate labels rounded to one decimal.',
                          'All ten players with 4+ goals are shown in FIFA scorer order; match penalties retained.'],24,35)
        s.text(64,1423,'Descriptive rates, without adjustments for opponents, role or playing conditions.',23,MUTED)
        s.text(64,1495,'Source: FIFA · Golden Boot table · Snapshot: 22 Oct 2026',22,MUTED)
        s.text(1136,1525,'meplar.com',20,MUTED,anchor='end')
    s.save(ROOT/FIFA/('chart_mobile.svg' if mobile else 'chart.svg'))

def aci_data():
    raw=json.loads((ROOT/'raw/aci-international-table-extract.json').read_text())['rows']
    assert raw[0][0]=='BUSIEST AIRPORTS BY INTERNATIONAL PASSENGER TRAFFIC in 2025*'
    assert raw[-1][0]=='* International passengers enplaned and deplaned'
    rows=[]
    for a in raw[2:-1]:
        code=re.search(r'\(([A-Z]{3})\)',a[3]).group(1)
        city=a[3].split(',')[0].title().replace('Hong Kong Sar','Hong Kong')
        rows.append({'rank_2025':int(a[0]),'rank_2024':int(a[1]),'rank_2019':int(a[2]),
                     'airport_code':code,'source_airport_label':a[3],'display_label':city+' '+code,
                     'international_passengers_2025':int(a[4].replace(' ','')),
                     'change_vs_2024_pct':Decimal(a[5]),'change_vs_2019_pct':Decimal(a[6]),
                     'below_2019':Decimal(a[6])<0,'shown':Decimal(a[6])<0,
                     'comparison_note':'2019 is affected by April airport transfer; not like for like. Excluded from plotted below-2019 cohort.' if code=='IST' else ''})
    assert len(rows)==20 and [r['rank_2025'] for r in rows]==list(range(1,21))
    assert len({r['airport_code'] for r in rows})==20
    selected=sorted([r for r in rows if r['below_2019']],key=lambda r:r['change_vs_2019_pct'])
    assert len(selected)==9 and sum(r['change_vs_2024_pct']>0 for r in selected)==8
    assert (selected[0]['airport_code'],selected[0]['change_vs_2024_pct'],selected[0]['change_vs_2019_pct'])==('HKG',Decimal('14.9'),Decimal('-14.7'))
    d=ROOT/ACI;d.mkdir(exist_ok=True);csv_write(d/'all-20-airports.csv',rows);csv_write(d/'chart-data-nine-below-2019.csv',selected)
    write_json(d/'metadata.json',{
        'status':'Unpublished candidate','source_url':'https://aci.aero/resources/busiest-airports-in-the-world/',
        'source_basis':'Original public HTML saved and hashed. International table parsed deterministically; not the overall-passenger or cargo tables.',
        'source_page_modified':'2026-10-15T14:54:35-04:00','retrieved':'2026-10-22','observation_year':2025,
        'universe':'ACI’s 20 busiest airports by 2025 international passenger traffic; this is a current-top-20 cohort, not all world airports or the 2019 top 20.',
        'display_cohort':'Exactly nine airports with published change_vs_2019_pct < 0. All 20 source rows remain in all-20-airports.csv.',
        'unit':'International passengers enplaned and deplaned, not unique travellers; percentage changes in annual airport traffic counts.',
        'comparisons':{'2025_vs_2024':'One-year percentage change','2025_vs_2019':'Six-year cumulative percentage change, not annualised'},
        'claims':{'below_2019_of_top20':9,'of_those_growing_vs2024':8,'Hong_Kong':'2025 +14.9% versus 2024, −14.7% versus 2019'},
        'istanbul':'ACI reports +68.3% vs 2019 for IST, but its 2019 baseline is affected by the Atatürk-to-Istanbul Airport transition on 6 April 2019. It is outside the negative-baseline cohort. No like-for-like long-term growth claim is made. Published rate retained in all-20 CSV.',
        'istanbul_primary_context':['https://aci.aero/2026/09/02/global-aviation-leaders-convene-in-istanbul-to-define-the-new-era-of-airport-experience/','https://www.igairport.aero/media/oltp5n1s/iga_srde_2019_uyg11.pdf'],
        'rounding':'Published percentages retained to one decimal. Do not reconstruct exact 2019/2024 counts from rounded percentages.',
        'layout':'Common −20% to +20% scale for both published rates in selected nine. Connector compares baselines, not a time-series trajectory.',
        'limits':'No causal attribution to COVID or any other universal explanation; airport comparisons can reflect different local circumstances.'})
    return selected

def render(folder):
    for name in ['chart.svg','chart_mobile.svg']:
        svg=ROOT/folder/name
        subprocess.run(['rsvg-convert','-z','2','-o',str(svg.with_suffix('.png')),str(svg)],check=True)
        width='320' if 'mobile' in name else '1200'
        subprocess.run(['rsvg-convert','-w',width,'-o',str(ROOT/'evidence'/(folder+('-mobile-320.png' if 'mobile' in name else '-desktop-1200.png'))),str(svg)],check=True)

def aci_chart(rows,mobile=False):
    w,h=(400,1620) if mobile else (1200,1490)
    s=SVG(w,h,'Nine major international airports still below 2019 in 2025',
          'Paired percentage changes for the nine of ACI’s 20 busiest international airports in 2025 whose traffic remained below 2019. Both rates share a −20% to +20% scale. Hong Kong: +14.9% versus 2024 and −14.7% versus 2019. Eight of these nine grew versus 2024. International passengers enplaned and deplaned, not unique travellers. All 20 airports are preserved in CSV. Istanbul is not in the displayed subset; its 2019 baseline is affected by the April 2019 airport transfer.')
    if mobile:
        s.text(24,36,'MEPLAR  /  INTERNATIONAL AIRPORTS',18,MUTED,700)
        s.lines(24,88,['Nine major airports','still below 2019'],32,39,weight=700)
        s.lines(24,175,['Of ACI’s top 20 international airports', 'by 2025 passenger traffic'],18,26,color=MUTED)
        s.text(24,240,'Hong Kong',20,weight=700)
        s.text(24,292,'+14.9%',44,TEAL,700)
        s.text(220,292,'−14.7%',44,RUST,700)
        s.text(24,321,'vs 2024',18,TEAL)
        s.text(220,321,'vs 2019',18,RUST)
        s.dot(31,365,TEAL,5);s.text(44,371,'2025 vs 2024',18,TEAL,700)
        s.dot(226,365,RUST,5,'square');s.text(240,371,'2025 vs 2019',18,RUST,700)
        s.text(24,420,'Airport',18,MUTED,700)
        s.text(278,420,'vs 2024',18,TEAL,700,anchor='end')
        s.text(376,420,'vs 2019',18,RUST,700,anchor='end')
        left,right,start,step=44,356,512,78
        x=lambda v:left+(float(v)+20)/40*(right-left)
        for tick in [-20,-10,0,10,20]:s.text(x(tick),456,('−' if tick<0 else '+' if tick>0 else '')+str(abs(tick))+'%',18,MUTED,anchor='middle')
    else:
        s.text(64,50,'MEPLAR  /  INTERNATIONAL AIRPORTS',20,MUTED,700)
        s.lines(64,128,['Nine major airports','still below 2019'],64,69,weight=700)
        s.text(64,257,'Of ACI’s 20 busiest airports by 2025 international passenger traffic',26,MUTED)
        s.text(64,315,'Hong Kong',25,weight=700)
        s.text(64,397,'+14.9%',76,TEAL,700);s.text(340,370,'vs 2024',25,TEAL)
        s.text(690,397,'−14.7%',76,RUST,700);s.text(979,370,'vs 2019',25,RUST)
        s.dot(71,450,TEAL,7);s.text(90,458,'2025 vs 2024 (one year)',23,TEAL,700)
        s.dot(667,450,RUST,7,'square');s.text(687,458,'2025 vs 2019 (six years)',23,RUST,700)
        s.text(64,513,'Airport',22,MUTED,700)
        s.text(982,513,'vs 2024',22,TEAL,700,anchor='end')
        s.text(1136,513,'vs 2019',22,RUST,700,anchor='end')
        left,right,start,step=360,830,560,64
        x=lambda v:left+(float(v)+20)/40*(right-left)
        for tick in [-20,-10,0,10,20]:
            s.line(x(tick),532,x(tick),1100,GRID if tick else '#a8b1b1',1 if tick else 2)
            s.text(x(tick),505,('−' if tick<0 else '+' if tick>0 else '')+str(abs(tick))+'%',20,MUTED,anchor='middle')
    for idx,r in enumerate(rows):
        y=start+idx*step
        if r['airport_code']=='HKG':s.rect(16 if mobile else 48,y-25,w-(32 if mobile else 96),step-3,'#f1e6dc',6)
        s.text(24 if mobile else 64,y+(0 if mobile else 8),r['display_label'],18 if mobile else 25,weight=700 if r['airport_code']=='HKG' else 400)
        yy=y+27 if mobile else y
        if mobile:
            s.line(left,yy,right,yy,GRID)
            s.line(x(0),yy-9,x(0),yy+9,'#a8b1b1',1.5)
        old,new=r['change_vs_2019_pct'],r['change_vs_2024_pct']
        s.line(x(old),yy,x(new),yy,'#8d9999',3 if mobile else 4)
        s.dot(x(new),yy,TEAL,5 if mobile else 7,extra=f'data-airport="{r["airport_code"]}" data-base="2024" data-value="{new}"')
        s.dot(x(old),yy,RUST,5 if mobile else 7,'square',extra=f'data-airport="{r["airport_code"]}" data-base="2019" data-value="{old}"')
        s.text(278 if mobile else 982,y+(0 if mobile else 8),signed(new)+'%',18 if mobile else 25,TEAL,700,anchor='end')
        s.text(376 if mobile else 1136,y+(0 if mobile else 8),signed(old)+'%',18 if mobile else 25,RUST,700,anchor='end')
    if mobile:
        s.line(24,1210,376,1210)
        s.text(24,1249,'Eight of these nine grew vs 2024.',18)
        s.lines(24,1290,['vs 2019 is a cumulative six-year change,','not an annual growth rate.'],18,26,color=MUTED)
        s.lines(24,1360,['International passengers enplaned and','deplaned; not unique travellers.'],18,26,color=MUTED)
        s.lines(24,1430,['Istanbul not shown: its 2019 baseline','includes the April airport transfer.'],18,26,color=MUTED)
        s.text(24,1500,'All 20 airports are in the CSV.',18,MUTED)
        s.lines(24,1550,['Source: ACI World · 2025 traffic','Snapshot: 22 Oct 2026 · meplar.com'],18,26,color=MUTED)
    else:
        s.line(64,1130,1136,1130)
        s.text(64,1176,'Eight of these nine grew versus 2024.',25)
        s.lines(64,1226,['Rates use different baselines; the 2019 comparison is cumulative, not annualised.',
                          'International passengers enplaned and deplaned; not unique travellers.'],23,34,color=MUTED)
        s.lines(64,1320,['Istanbul (IST) is outside this selection. Its 2019 baseline is affected by the April airport transfer.',
                          'All 20 airports and published rates are included in the CSV.'],21,31,color=MUTED)
        s.text(64,1412,'Source: ACI World · 2025 international passenger table · Snapshot: 22 Oct 2026',22,MUTED)
        s.text(1136,1456,'meplar.com',20,MUTED,anchor='end')
    s.save(ROOT/ACI/('chart_mobile.svg' if mobile else 'chart.svg'))

def main():
    for record in json.loads((ROOT/'source-capture.json').read_text()):
        if record.get('distribution')=='include':
            p=ROOT/'raw'/record['file'];assert hashlib.sha256(p.read_bytes()).hexdigest()==record['sha256'],p.name
    if '--only-airports' not in sys.argv:
        rows=fifa_data()
        for m in [False,True]:fifa_chart(rows,m)
        render(FIFA)
    if '--only-fifa' not in sys.argv:
        airports=aci_data()
        for m in [False,True]:aci_chart(airports,m)
        render(ACI)
    print('Both fresh candidates rendered: desktop/mobile SVG and 2x PNG; data and metadata ready.')

if __name__=='__main__':main()
