"""Plot only the eight saved Case26 results; no scientific recalculation.
Requires existing matplotlib. Run:
python case26-reporting-ratios-20261012-plot.py --csv reporting-results.csv --bundle reproduction-bundle-corrected-2026-10-08.zip
Outputs refuse overwrite. SVG text uses DejaVu Sans; rendering depends on fonts.
"""
import argparse, csv, io, hashlib, zipfile, math, json
from pathlib import Path
from decimal import Decimal
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.transforms import blended_transform_factory
from matplotlib.ticker import FixedLocator, FixedFormatter, NullLocator

NATIVE_STYLE = json.loads("{\"axes.labelsize\":8,\"axes.linewidth\":0.6,\"axes.spines.right\":false,\"axes.spines.top\":false,\"axes.titlelocation\":\"left\",\"axes.titlesize\":8,\"figure.dpi\":200,\"font.size\":8,\"legend.fontsize\":7,\"legend.frameon\":false,\"lines.linewidth\":1.2,\"patch.linewidth\":0.6,\"pdf.fonttype\":42,\"ps.fonttype\":42,\"savefig.bbox\":\"tight\",\"savefig.dpi\":300,\"xtick.labelsize\":6,\"xtick.major.size\":3,\"xtick.major.width\":0.6,\"ytick.labelsize\":6,\"ytick.major.size\":3,\"ytick.major.width\":0.6}")

def main(csv_path, bundle_path, prefix):
    csv_path, bundle_path, prefix = Path(csv_path), Path(bundle_path), Path(prefix)
    paths = {ext:Path(str(prefix)+ext) for ext in ['.png','.svg','-source-check.csv']}
    for p in paths.values():
        if p.exists(): raise FileExistsError('Refusing overwrite: '+str(p))
    raw=csv_path.read_bytes()
    csv_hash=hashlib.sha256(raw).hexdigest()
    bundle_hash=hashlib.sha256(bundle_path.read_bytes()).hexdigest()
    with zipfile.ZipFile(bundle_path) as z:
        member=z.read('reference/reporting-results.csv')
    assert raw==member, 'Saved source CSVs disagree'
    assert csv_hash=='4843b539aaad2d661d3375f1aee89f8a46c21c951548c88e8aacd24817cd7de2'
    assert bundle_hash=='9cf78a32a741b86365c49f52d0f0a5546e12b13caaf2d08c9411365e09ea1e63'
    reader=csv.DictReader(io.StringIO(raw.decode('utf-8')))
    original_columns=list(reader.fieldnames)
    rows=list(reader)
    assert len(rows)==8
    keyed={(r['analysis'],r['medicine'],r['reaction_pt']):r for r in rows}
    assert len(keyed)==8
    groups=['primary_any_role','sensitivity_ps_ss']
    medicines=['semaglutide','liraglutide']
    terms=['PANCREATITIS','CHOLELITHIASIS']
    ordered=[keyed[(g,m,t)] for g in groups for m in medicines for t in terms]
    assert {r['n_eligible'] for r in rows}=={'422458'}
    for r in ordered:
        lo,x,hi=[float(r[k]) for k in ['ci95_low','ror','ci95_high']]
        assert all(math.isfinite(v) and v>0 for v in [lo,x,hi])
        assert lo<=x<=hi
    # Style is explicit for offline reproduction; no external style helper required.
    style={**NATIVE_STYLE,'font.family':'DejaVu Sans','font.size':12,'axes.labelsize':12,
           'xtick.labelsize':10,'ytick.labelsize':10,'figure.facecolor':'white',
           'axes.facecolor':'white','savefig.facecolor':'white','text.color':'#20252B',
           'axes.edgecolor':'#737373','svg.fonttype':'none','svg.hashsalt':'case26-20261012'}
    with mpl.rc_context(style):
        fig=plt.figure(figsize=(14,9),dpi=150,facecolor='white')
        FigureCanvasAgg(fig)
        ax=fig.add_axes([0.445,0.26,0.30,0.55])
        ax.set_xscale('log')
        ax.set_xlim(0.8,22)
        ax.set_ylim(-0.65,10.1)
        ax.xaxis.set_major_locator(FixedLocator([1,2,5,10,20]))
        ax.xaxis.set_major_formatter(FixedFormatter(['1','2','5','10','20']))
        ax.xaxis.set_minor_locator(NullLocator())
        ax.set_yticks([])
        ax.set_xlabel('Reporting odds ratio (log scale)',labelpad=12)
        for side in ['top','left','right']: ax.spines[side].set_visible(False)
        ax.spines['bottom'].set_color('#777777')
        ax.grid(axis='x',color='#E4E7EB',linewidth=0.7,zorder=0)
        reference=ax.axvline(1,color='#555555',linestyle=(0,(4,3)),linewidth=1.2,zorder=1)
        assert list(reference.get_xdata())==[1,1]
        blend=blended_transform_factory(fig.transFigure,ax.transData)
        text_artists=[]
        def ft(x,y,s,**kw):
            obj=fig.text(x,y,s,**kw); text_artists.append(obj); return obj
        def rt(x,y,s,**kw):
            obj=ax.text(x,y,s,transform=blend,clip_on=False,**kw)
            text_artists.append(obj); return obj
        ft(0.04,0.946,'Reporting odds ratios for selected GLP-1 medicines',fontsize=18,weight='bold',va='top')
        ft(0.04,0.899,'FDA FAERS 2026 Q2  |  422458 eligible unique cases',fontsize=12,va='top')
        rt(0.04,9.75,'Medicine',fontsize=12,weight='bold',va='center')
        rt(0.18,9.75,'Reaction term',fontsize=12,weight='bold',va='center')
        rt(0.375,9.75,'Drug-event\ncount (a)',fontsize=10,weight='bold',ha='center',va='center')
        rt(0.875,9.75,'ROR (saved 95% CI)',fontsize=12,weight='bold',ha='center',va='center')
        colors={'primary_any_role':'#00689D','sensitivity_ps_ss':'#985000'}
        markers={'primary_any_role':'o','sensitivity_ps_ss':'D'}
        rt(0.04,8.95,'Primary: any drug role',fontsize=12,weight='bold',color=colors[groups[0]],va='center')
        rt(0.965,8.95,'primary_any_role',fontsize=10,color=colors[groups[0]],ha='right',va='center')
        rt(0.04,3.95,'Sensitivity: primary/secondary suspect',fontsize=12,weight='bold',color=colors[groups[1]],va='center')
        rt(0.965,3.95,'sensitivity_ps_ss',fontsize=10,color=colors[groups[1]],ha='right',va='center')
        ypos=[8,7,6,5,3,2,1,0]
        drawn=[]; checks=[]
        for index,(r,y) in enumerate(zip(ordered,ypos),start=1):
            g=r['analysis']; color=colors[g]
            lo,x,hi=[float(r[k]) for k in ['ci95_low','ror','ci95_high']]
            point,=ax.plot([x],[y],marker=markers[g],linestyle='none',markersize=7,color=color,zorder=3)
            interval=ax.hlines(y,lo,hi,color=color,linewidth=1.8,zorder=2)
            ax.vlines([lo,hi],y-0.075,y+0.075,color=color,linewidth=1.3,zorder=2)
            medicine_label=r['medicine'].capitalize()
            count_label=r['a_drug_event']
            estimate_label=f"{Decimal(r['ror']):.2f} ({Decimal(r['ci95_low']):.2f}-{Decimal(r['ci95_high']):.2f})"
            med_text=rt(0.04,y,medicine_label,fontsize=12,va='center')
            term_text=rt(0.18,y,r['reaction_pt'],fontsize=10,va='center')
            count_text=rt(0.375,y,count_label,fontsize=12,ha='center',va='center')
            estimate_text=rt(0.875,y,estimate_label,fontsize=12,ha='center',va='center')
            assert float(point.get_xdata()[0])==float(r['ror'])
            segment=interval.get_segments()[0]
            assert float(segment[0,0])==float(r['ci95_low']) and float(segment[1,0])==float(r['ci95_high'])
            assert count_text.get_text()==r['a_drug_event']
            assert term_text.get_text()==r['reaction_pt'] and med_text.get_text().lower()==r['medicine']
            drawn.append((point,interval,count_text,estimate_text))
            checks.append({**r,'plot_row':index,'display_ror_ci':estimate_label,'display_drug_event_count':count_label,
                'standalone_csv_sha256':csv_hash,'bundle_member_csv_sha256':hashlib.sha256(member).hexdigest(),
                'corrected_bundle_sha256':bundle_hash,'standalone_csv_source':str(csv_path),
                'bundle_source':str(bundle_path),'bundle_member':'reference/reporting-results.csv',
                'sources_byte_identical':'True','plotted_saved_values_verified':'True'})
        # Three short caption lines, with the sparse-count warning stated explicitly.
        ft(0.04,0.172,'Points: saved RORs; bars: saved 95% CIs. Dashed line: ROR = 1. Same full eligible background in both analyses.',fontsize=10,va='top')
        ft(0.04,0.130,'Sparse liraglutide counts: 5 pancreatitis and 7 cholelithiasis drug-event cases in each analysis; interpret cautiously.',fontsize=10,va='top')
        ft(0.04,0.088,'Reporting patterns only: not incidence, causality, or head-to-head comparative safety.',fontsize=12,weight='bold',va='top')
        ft(0.04,0.044,'Visualization of eight saved rows only. No estimates, confidence intervals, or p-values recalculated.',fontsize=10,va='top')
        fig.canvas.draw()
        renderer=fig.canvas.get_renderer()
        texts=[(t,t.get_window_extent(renderer)) for t in fig.findobj(mpl.text.Text)
               if t.get_visible() and t.get_text().strip()]
        clipping=[t.get_text() for t,b in texts if b.x0<0 or b.y0<0 or b.x1>fig.bbox.width or b.y1>fig.bbox.height]
        overlaps=[(a.get_text(),b.get_text()) for i,(a,ba) in enumerate(texts) for b,bb in texts[i+1:] if ba.overlaps(bb)]
        ticklabels=set(ax.get_xticklabels(which='both')+ax.get_yticklabels(which='both'))
        spine_overlaps=[t.get_text() for t,bt in texts for s in ax.spines.values()
                        if s.get_visible() and t not in ticklabels and bt.overlaps(s.get_window_extent(renderer))]
        assert not clipping, clipping
        assert not overlaps, overlaps
        assert not spine_overlaps, spine_overlaps
        for c in checks:
            c.update({'text_clipping_check':'passed','text_overlap_check':'passed','text_spine_overlap_check':'passed'})
        fig.savefig(paths['.png'],dpi=300,facecolor='white')
        fig.savefig(paths['.svg'],facecolor='white',metadata={'Date':None,'Description':'Case26: eight saved reporting odds ratios and confidence intervals; visualization only.'})
        extras=[k for k in checks[0] if k not in original_columns]
        with paths['-source-check.csv'].open('x',newline='',encoding='utf-8') as f:
            writer=csv.DictWriter(f,fieldnames=original_columns+extras)
            writer.writeheader(); writer.writerows(checks)
        saved_checks=list(csv.DictReader(io.StringIO(paths['-source-check.csv'].read_text(encoding='utf-8'))))
        for r,c in zip(ordered,saved_checks):
            assert all(r[k]==c[k] for k in original_columns)
        assert csv_path.read_bytes()==raw
        assert hashlib.sha256(bundle_path.read_bytes()).hexdigest()==bundle_hash
        print(json.dumps({'rows':8,'all_original_fields_preserved':True,'source_hashes_verified':True,
            'displayed_estimates_intervals_counts_verified':True,'text_clipping':clipping,'text_overlaps':overlaps,
            'text_spine_overlaps':spine_overlaps,'outputs':{k:str(v) for k,v in paths.items()},
            'matplotlib_version':mpl.__version__},indent=2))
        return fig,checks
if __name__=='__main__':
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--csv',default='reporting-results.csv')
    parser.add_argument('--bundle',default='reproduction-bundle-corrected-2026-10-08.zip')
    parser.add_argument('--prefix',default='case26-reporting-ratios-20261012')
    args=parser.parse_args()
    main(args.csv,args.bundle,args.prefix)
