#!/usr/bin/env python3
import os, re, sqlite3, math, html
from datetime import datetime
from pathlib import Path
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak, KeepTogether
from reportlab.pdfbase.pdfmetrics import stringWidth

DB='/home/agent/.hermes/assets/Gesundheit/health_data.db'
OUTDIR=Path('/home/agent/Gesundheit/reports/hyrimoz_lab_monitoring')
OUTDIR.mkdir(parents=True, exist_ok=True)
DATE=datetime.now().strftime('%Y-%m-%d')
PDF=str(OUTDIR/f'Hyrimoz_Behcet_Laborkontrolle_Arztgespraech_{DATE}.pdf')
HTML=str(OUTDIR/f'Hyrimoz_Behcet_Laborkontrolle_Arztgespraech_{DATE}.html')
IMGDIR=OUTDIR/'charts'; IMGDIR.mkdir(exist_ok=True)
MIN_DATE = pd.Timestamp('2024-01-01')

# Approximate adult/reference or decision ranges used only for visual orientation.
# Lab-specific reference intervals remain authoritative. None = open bound.
REF_RANGES = {
    'CRP': (None, 5.0),
    'Leukozyten': (3.5, 10.0),
    'Hämoglobin': (12.0, 16.5),
    'Hämatokrit': (35.0, 50.0),
    'Thrombozyten': (150.0, 390.0),
    'Kreatinin': (62.0, 106.0),
    'eGFR': (60.0, None),
    'ALT/ALAT': (None, 50.0),
    'AST/ASAT': (None, 50.0),
    'GGT': (None, 60.0),
    'Alk. Phosphatase': (40.0, 130.0),
    'Bilirubin gesamt': (None, 21.0),
    'Phosphat': (0.87, 1.45),
    'Gesamtcholesterin': (None, 5.0),
    'LDL-Cholesterin': (None, 3.0),
    'non-HDL-Cholesterin': (None, 3.9),
    'Triglyceride': (None, 2.0),
    'Glukose': (3.9, 5.6),
    'HbA1c': (None, 5.7),
    'Protein/Kreatinin Urin': (None, 15.0),
    'Albumin/Kreatinin Urin': (None, 3.0),
    'spez. Gewicht Urin': (1015.0, 1025.0),
    'Urin Erythrozyten': (None, 5.0),
    'Fibrinogen': (1.8, 3.5),
    'Faktor VIII': (50.0, 150.0),
}

SOURCES = {
    'EULAR2018': 'Hatemi G et al. 2018 update of the EULAR recommendations for the management of Behçet’s syndrome. Ann Rheum Dis. 2018;77:808–818. PMID: 29625968.',
    'MAJOR2018': 'Leccese P et al. Management of major organ involvement of Behçet’s syndrome: systematic review for EULAR update. Rheumatology. 2018. PMID: 30107448.',
    'EMMI2018': 'Emmi G et al. Adalimumab-based treatment versus DMARDs for venous thrombosis in Behçet’s syndrome: retrospective 70-patient study. Arthritis Rheumatol. 2018. PMID: 29676522.',
    'EROL2024': 'Erol F et al. Does anticoagulation in combination with immunosuppressive therapy prevent recurrent thrombosis in Behçet’s disease? Clin Rheumatol. 2024. PMID: 38357865.',
    'EMA_HYRIMOZ': 'European Medicines Agency: Hyrimoz EPAR / Product information (adalimumab biosimilar): contraindications, infections, TB/HBV screening, monitoring warnings.',
    'FDA_HUMIRA': 'US FDA Humira (adalimumab) Prescribing Information: serious infections, TB testing before/during therapy, HBV reactivation, malignancy warnings, hematologic/hepatic events.',
    'EULAR_VACC': 'Furer V et al. 2019 update of EULAR recommendations for vaccination in adult patients with autoimmune inflammatory rheumatic diseases. Ann Rheum Dis. 2020. PMID: 31413005.',
    'DOAC_SPS': 'NHS Specialist Pharmacy Service: DOACs monitoring guidance — baseline and periodic FBC, liver function, renal function; assess bleeding/anemia and renal-dose suitability.',
    'RENAL_BEHCET': 'Akpolat T et al. Renal Behçet’s disease: an update. Semin Arthritis Rheum. 2008/related reviews; renal involvement can include hematuria/proteinuria/glomerulonephritis, supporting urinalysis when systemic disease is active.',
    'KDIGO_ALBUMIN': 'KDIGO CKD guidance: albuminuria/proteinuria and eGFR are core kidney damage markers; spot urine albumin-creatinine/protein-creatinine ratios quantify renal involvement.',
    'CV_EULAR': 'EULAR cardiovascular-risk management principles in inflammatory rheumatic disease: systemic inflammation and glucocorticoid exposure can modify CV risk; lipid/metabolic risk should be assessed.'
}

RECOMMENDATIONS = [
    ('Sehr hoch', 'CRP + BSG/ESR', 'Krankheitsaktivität / Entzündung', 'Baseline und Verlauf unter Hyrimoz: zeigt, ob Entzündung objektiv sinkt; bei vascular Behçet wichtig, weil Thrombosen oft inflammation-driven sind.', 'EULAR2018; EMMI2018; MAJOR2018'),
    ('Sehr hoch', 'Grosses Blutbild mit Differential', 'Safety + Entzündung', 'TNF-Hemmer können selten hämatologische Auffälligkeiten/Infekte maskieren; Leukozyten/Neutrophile, Hb und Thrombozyten helfen Infekt, Anämie/Blutung und Entzündungsreaktion zu erkennen.', 'FDA_HUMIRA; EMA_HYRIMOZ; DOAC_SPS'),
    ('Sehr hoch', 'Leberwerte: ALT, AST, GGT, AP, Bilirubin', 'Hyrimoz-/Medikamenten-Safety', 'Adalimumab kann selten Leberreaktionen/Hepatitis-B-Reaktivierung triggern; Basis und Verlauf sind sinnvoll, besonders bei Kombimedikation.', 'FDA_HUMIRA; EMA_HYRIMOZ'),
    ('Sehr hoch', 'Nierenfunktion: Kreatinin, eGFR, Harnstoff, Elektrolyte', 'Eliquis-Safety + systemische Kontrolle', 'Apixaban-Sicherheit/Dosis hängt u.a. von Nierenfunktion ab; Behçet kann selten renal beteiligt sein; Verlauf wichtig.', 'DOAC_SPS; RENAL_BEHCET; KDIGO_ALBUMIN'),
    ('Sehr hoch', 'Urinstatus/Sediment: Erythrozyten, Leukozyten, Protein, Blut; plus Albumin/Kreatinin und Protein/Kreatinin', 'Nieren-/Vaskulitis-Screening', 'Screening auf Hämaturie/Proteinurie als Hinweis auf renale Beteiligung oder Blutung unter Antikoagulation; quantifizierbare Ratios sind besser als Streifentest allein.', 'RENAL_BEHCET; KDIGO_ALBUMIN; DOAC_SPS'),
    ('Sehr hoch', 'TB-Screening: IGRA/Quantiferon ± Thorax nach ärztl. Ermessen', 'Vor/unter TNF-Hemmer', 'TNF-Blockade erhöht Risiko für Reaktivierung latenter Tuberkulose; vor Start muss ausgeschlossen/behandelt werden, bei Exposition/Symptomen wiederholen.', 'FDA_HUMIRA; EMA_HYRIMOZ'),
    ('Sehr hoch', 'Hepatitis B: HBsAg, anti-HBc, anti-HBs; HCV; HIV', 'Infektions-Safety vor Biologic', 'HBV-Reaktivierung ist unter TNF-Hemmern beschrieben; HCV/HIV als relevante Basisinfektionen vor Immunsuppression.', 'FDA_HUMIRA; EMA_HYRIMOZ'),
    ('Hoch', 'Fibrinogen, D-Dimer, Faktor VIII — nur wenn klinisch sinnvoll/bei Symptomen oder zur Hämostase-Verlaufsfrage', 'Thrombose-/Entzündungs-Kontext', 'Nicht als Routine-“alles klar”-Marker geeignet, aber bei vascular Behçet/Thrombose-Frage als Vergleich oder bei neuen Symptomen hilfreich; Faktor VIII/Fibrinogen können Entzündung/Prothrombose reflektieren.', 'EULAR2018; EROL2024'),
    ('Hoch', 'Lipidprofil: Gesamt, LDL, HDL, non-HDL, Triglyceride', 'Vaskuläres Gesamtrisiko', 'Chronische Entzündung und Steroide erhöhen CV-Risiko; bei vascular phenotype LDL/non-HDL als modifizierbarer Risikofaktor relevant.', 'CV_EULAR'),
    ('Mittel', 'HbA1c / Nüchternglukose', 'Metabolisches Risiko', 'Steroidexposition, Entzündung und kardiovaskuläres Risiko rechtfertigen periodische Kontrolle; Glukose war in der DB teils erhöht.', 'CV_EULAR'),
    ('Mittel', 'Immunglobuline IgG/IgA/IgM — wenn rezidivierende Infekte/ungewöhnliche Infektneigung', 'Infektanfälligkeit einordnen', 'Nicht zwingend Routine vor jedem TNF-Hemmer, aber bei Infektanfälligkeit als Baseline nützlich.', 'EMA_HYRIMOZ; FDA_HUMIRA'),
    ('Mittel', 'Ferritin, Eisenstatus, B12/Folat, Vitamin D — symptomorientiert', 'Fatigue/Anämie/Knochen-Immunsystem', 'Nicht Hyrimoz-spezifisch, aber bei Müdigkeit, niedrigerem Hb, Ernährungsrestriktionen oder Steroid-/Entzündungsbelastung sinnvoll.', 'klinische Basisdiagnostik'),
]

ALIASES = [
    ('CRP', [r'^CRP$', r'C[- ]?reaktives Protein', r'C-Reaktives Protein \(CRP\)']),
    ('Leukozyten', [r'^Leukozyten$']),
    ('Hämoglobin', [r'^Hämoglobin$']),
    ('Hämatokrit', [r'^Hämatokrit$']),
    ('Thrombozyten', [r'^Thrombozyten$']),
    ('Kreatinin', [r'^Kreatinin$']),
    ('eGFR', [r'^eGFR', r'eGFR \(Niere\)']),
    ('ALT/ALAT', [r'^ALT$', r'ALAT']),
    ('AST/ASAT', [r'ASAT', r'AST']),
    ('GGT', [r'^GGT$']),
    ('Alk. Phosphatase', [r'Alk\. Phosphatase']),
    ('Bilirubin gesamt', [r'Bilirubin']),
    ('Albumin', [r'^Albumin$']),
    ('Phosphat', [r'^Phosphat$']),
    ('Gesamtcholesterin', [r'Cholesterin gesamt', r'Cholesterin total', r'^Cholesterin$']),
    ('LDL-Cholesterin', [r'LDL']),
    ('non-HDL-Cholesterin', [r'^(?:Non|non)-HDL', r'^(?:Non|non)-HDL-Cholesterin']),
    ('HDL-Cholesterin', [r'^HDL-Cholesterin$']),
    ('Triglyceride', [r'Triglyceride']),
    ('Glukose', [r'^Glukose$']),
    ('HbA1c', [r'HbA1c']),
    ('D-Dimer', [r'D-Dimer', r'D-Dimere']),
    ('Fibrinogen', [r'Fibrinogen']),
    ('Faktor VIII', [r'Faktor VIII']),
    ('Protein/Kreatinin Urin', [r'Protein/Kreatinin']),
    ('Albumin/Kreatinin Urin', [r'Albumin/Kreatinin']),
    ('IgG/Kreatinin Urin', [r'IgG/Kreatinin']),
    ('spez. Gewicht Urin', [r'spez', r'Spezifisches Gewicht']),
    ('Urin Erythrozyten', [r'Ery \(isomorph\)']),
    ('Urin Leukozyten', [r'^Leukozyten$']),
    ('HIV Ag/Ak', [r'HIV Ag/Ak']),
    ('HBs-Antigen', [r'HBs-Antigen']),
    ('Anti-HBs', [r'^Anti-HBs$']),
    ('Anti-HBs quant', [r'Anti-HBs quant']),
    ('Anti-HBc-total', [r'Anti-HBc-total']),
    ('Anti-HCV', [r'Anti-HCV']),
    ('Vitamin D', [r'Vitamin D']),
    ('Ferritin', [r'Ferritin']),
    ('Vitamin B12 aktiv', [r'Vitamin B12']),
]

def canon(name):
    for c, pats in ALIASES:
        for p in pats:
            if re.search(p, name or '', re.I):
                return c
    return name

def parse_num(x):
    if x is None: return np.nan
    s=str(x).strip().replace(',', '.')
    if not s or re.search(r'\d{4}-\d{2}-\d{2}', s): return np.nan
    m=re.search(r'[-+]?\d+(?:\.\d+)?', s)
    if not m: return np.nan
    val=float(m.group(0))
    return val

def date_from(row):
    # Prefer explicit measurement/sample date from the current HealthManager DB.
    # ermittlung_datum is often only import time; abnahme_datum is the clinically relevant date.
    for key in ['abnahme_datum', 'befund_datum']:
        val = row.get(key) if hasattr(row, 'get') else None
        if val not in (None, ''):
            try:
                ts = pd.to_datetime(val, errors='coerce')
                if not pd.isna(ts):
                    return ts.normalize()
            except Exception:
                pass
    txt=' '.join(str(row.get(k) or '') for k in ['bemerking','datei_name','quelle'])
    # dd.mm.yyyy
    m=re.search(r'(\d{1,2})\.(\d{1,2})\.(20\d{2})', txt)
    if m:
        return pd.Timestamp(year=int(m.group(3)), month=int(m.group(2)), day=int(m.group(1)))
    # filename yymmdd
    m=re.search(r'(^|[^0-9])(\d{2})(\d{2})(\d{2})(?=[^0-9])', txt)
    if m:
        yy=int(m.group(2)); mm=int(m.group(3)); dd=int(m.group(4))
        if 1<=mm<=12 and 1<=dd<=31:
            return pd.Timestamp(year=2000+yy, month=mm, day=dd)
    try:
        return pd.to_datetime(row.get('ermittlung_datum')).normalize()
    except Exception:
        return pd.NaT

con=sqlite3.connect(DB)
raw=pd.read_sql_query('''select l.*, d.datei_name from laborwerte l left join dokumente d on d.id=l.dokument_id''', con)
# Harmonise optional schema fields across old/local and current HealthManager DBs.
for col in ['abnahme_datum', 'befund_datum', 'quelle']:
    if col not in raw.columns:
        raw[col] = None
# Remove legacy artefact rows without a true measurement date/source; the current HealthManager DB
# also contains validated rows with abnahme_datum for the same values. Keeping both makes imported
# reference-XLSX values look newer than they are.
raw = raw[~(raw['abnahme_datum'].isna() & raw['quelle'].isna() & raw['dokument_id'].isna())].copy()
raw['canonical']=raw['parameter_name'].map(canon)
raw['date']=raw.apply(date_from, axis=1)
raw['num']=raw['wert'].map(parse_num)
# Korrekturen für importierte Viollier-Urinzeilen ohne eindeutigen Namen/Kontext
jan28 = raw['date'].dt.strftime('%Y-%m-%d').eq('2026-01-28')
raw.loc[jan28 & raw['parameter_name'].eq('Leukozyten') & raw['wert'].astype(str).str.match(r'^\s*1(?:\.0)?\s*$'), 'canonical']='Urin Leukozyten'
raw.loc[jan28 & raw['parameter_name'].eq('Albumin') & raw['wert'].astype(str).str.contains('<', regex=False), 'canonical']='Albumin Urin'
raw.loc[jan28 & raw['parameter_name'].eq('IgG') & raw['wert'].astype(str).str.contains('<', regex=False), 'canonical']='IgG Urin'
raw['display_value']=raw['wert'].fillna('').astype(str) + raw['einheit'].fillna('').astype(str).map(lambda u: (' '+u) if u else '')
# crude urine context: Leukozyten name collides; rows around urin by dokument/notes
raw.loc[(raw['parameter_name'].eq('Leukozyten')) & (raw['bemerking'].fillna('').str.contains('Urin|Viollier 28.01.2026', case=False, regex=True)), 'canonical']='Urin Leukozyten'
# Drop clearly bad parsed date-valued lab values and empty numeric rows for plots
raw=raw[~raw['date'].isna()].copy()
raw['source_doc']=raw['datei_name'].fillna(raw['quelle']).fillna('manuell/DB')
raw['source_note']=raw.apply(lambda rr: (str(rr.get('quelle')) if pd.notna(rr.get('quelle')) and str(rr.get('quelle')).strip() else str(rr.get('source_doc') or '')), axis=1)
raw['bemerking_clean']=raw['bemerking'].apply(lambda x: '' if pd.isna(x) or str(x).lower()=='nan' else str(x))
# Werte vor 2024 für diesen Bericht ignorieren (Wunsch: nur aktueller Verlauf)
raw=raw[raw['date'] >= MIN_DATE].copy()
# remove exact duplicate imports: same canonical/date/display preferred latest row but unique
raw=raw.sort_values(['date','id'])
raw_dedup=raw.drop_duplicates(subset=['canonical','date','wert','einheit'], keep='last')

# Current/latest for each key canonical, prefer non-empty value and latest actual date
cur_rows=[]
for c in sorted(raw_dedup['canonical'].dropna().unique()):
    sub=raw_dedup[(raw_dedup['canonical']==c) & (raw_dedup['wert'].fillna('').astype(str).str.strip()!='')].sort_values(['date','id'])
    # fehlerhafte Imports, bei denen der Wert versehentlich als Datum gespeichert wurde, nicht als aktueller Laborwert anzeigen
    sub=sub[~sub['wert'].astype(str).str.contains(r'20\d{2}-\d{2}-\d{2}', regex=True, na=False)]
    if len(sub):
        r=sub.iloc[-1]
        cur_rows.append({
            'Parameter': c, 'Aktuell': r['display_value'], 'Datum': r['date'].strftime('%d.%m.%Y'),
            'Referenz': (lambda mn,mx: '' if (str(mn) in ['', 'None', 'nan'] and str(mx) in ['', 'None', 'nan']) else (str(mn) if str(mn) not in ['None','nan'] else '')+'–'+(str(mx) if str(mx) not in ['None','nan'] else ''))(r.get('reference_min') or '', r.get('reference_max') or ''),
            'Bemerkung': (str(r.get('source_note') or '') + (' · ' if str(r.get('source_note') or '').strip() and str(r.get('bemerking_clean') or '').strip() else '') + str(r.get('bemerking_clean') or ''))[:100]
        })
current=pd.DataFrame(cur_rows)

# Relevant current table order
ORDER=['CRP','Leukozyten','Hämoglobin','Hämatokrit','Thrombozyten','Kreatinin','eGFR','ALT/ALAT','AST/ASAT','GGT','Alk. Phosphatase','Bilirubin gesamt','Phosphat','D-Dimer','Fibrinogen','Faktor VIII','Gesamtcholesterin','LDL-Cholesterin','HDL-Cholesterin','non-HDL-Cholesterin','Triglyceride','Glukose','HbA1c','Protein/Kreatinin Urin','Albumin/Kreatinin Urin','spez. Gewicht Urin','Urin Erythrozyten','HIV Ag/Ak','HBs-Antigen','Anti-HBs quant','Anti-HBc-total','Anti-HCV']
current=current[current['Parameter'].isin(ORDER)].copy()
current['ord']=current['Parameter'].map({v:i for i,v in enumerate(ORDER)})
current=current.sort_values('ord').drop(columns='ord')

# Create charts for groups
plt.rcParams.update({'font.size':8.5, 'axes.titlesize':9.5, 'figure.dpi':170})
chart_groups = {
    '01_entzuendung_blutbild': ['CRP','Leukozyten','Thrombozyten','Hämoglobin'],
    '02_niere_urin': ['Kreatinin','eGFR','Protein/Kreatinin Urin','Albumin/Kreatinin Urin'],
    '03_leber': ['ALT/ALAT','AST/ASAT','GGT','Alk. Phosphatase','Bilirubin gesamt'],
    '04_lipide_metabolik': ['Gesamtcholesterin','LDL-Cholesterin','HDL-Cholesterin','non-HDL-Cholesterin','Triglyceride','Glukose','HbA1c'],
    '05_thrombose_marker': ['D-Dimer','Fibrinogen','Faktor VIII'],
}

def status_for_value(param, val):
    lo, hi = REF_RANGES.get(param, (None, None))
    if pd.isna(val) or (lo is None and hi is None):
        return '—', '#e5e7eb'
    if lo is not None and val < lo:
        return ('niedrig', '#f59e0b') if val >= lo*0.9 else ('niedrig', '#ef4444')
    if hi is not None and val > hi:
        return ('erhöht', '#f59e0b') if val <= hi*1.2 else ('erhöht', '#ef4444')
    return 'Norm/OK', '#22c55e'

def visual_limits(param, vals):
    vals = [float(v) for v in vals if not pd.isna(v)]
    lo_ref, hi_ref = REF_RANGES.get(param, (None, None))
    candidates = list(vals)
    if lo_ref is not None: candidates.append(float(lo_ref))
    if hi_ref is not None: candidates.append(float(hi_ref))
    if not candidates: return 0, 1
    ymin, ymax = min(candidates), max(candidates)
    span = ymax - ymin
    if span == 0:
        span = max(abs(ymax)*0.08, 1.0)
    pad = max(span*0.22, abs(ymax)*0.025, 0.2)
    ymin -= pad; ymax += pad
    if ymin > 0 and (lo_ref is None or lo_ref >= 0): ymin = max(0, ymin)
    return ymin, ymax

def shade_reference(ax, param, ymin, ymax):
    lo, hi = REF_RANGES.get(param, (None, None))
    if lo is None and hi is None:
        return
    if lo is not None and hi is not None:
        ax.axhspan(ymin, max(ymin, lo), color='#fee2e2', alpha=0.45, zorder=0)
        ax.axhspan(max(ymin, lo), min(ymax, hi), color='#dcfce7', alpha=0.55, zorder=0)
        ax.axhspan(min(ymax, hi), ymax, color='#fee2e2', alpha=0.45, zorder=0)
        ax.axhline(lo, color='#f97316', linewidth=0.9, alpha=.75, zorder=1)
        ax.axhline(hi, color='#f97316', linewidth=0.9, alpha=.75, zorder=1)
    elif hi is not None:
        orange_hi = hi * 1.2 if hi != 0 else hi + 1
        ax.axhspan(ymin, min(ymax, hi), color='#dcfce7', alpha=0.55, zorder=0)
        ax.axhspan(max(ymin, hi), min(ymax, orange_hi), color='#ffedd5', alpha=0.55, zorder=0)
        ax.axhspan(max(ymin, orange_hi), ymax, color='#fee2e2', alpha=0.50, zorder=0)
        ax.axhline(hi, color='#f97316', linewidth=0.9, alpha=.75, zorder=1)
    elif lo is not None:
        orange_lo = lo * 0.9
        ax.axhspan(ymin, min(ymax, orange_lo), color='#fee2e2', alpha=0.50, zorder=0)
        ax.axhspan(max(ymin, orange_lo), min(ymax, lo), color='#ffedd5', alpha=0.55, zorder=0)
        ax.axhspan(max(ymin, lo), ymax, color='#dcfce7', alpha=0.55, zorder=0)
        ax.axhline(lo, color='#f97316', linewidth=0.9, alpha=.75, zorder=1)

chart_paths=[]
for fname, params in chart_groups.items():
    present=[]
    for p in params:
        sub=raw_dedup[(raw_dedup['canonical']==p) & (~raw_dedup['num'].isna()) & (raw_dedup['date']>=MIN_DATE)].copy()
        sub=sub[~sub['wert'].astype(str).str.contains(r'20\d{2}-\d{2}-\d{2}', regex=True, na=False)]
        sub=sub.drop_duplicates(['date','num']).sort_values('date')
        if len(sub)>=1: present.append((p,sub))
    if not present: continue
    n=len(present); cols=2; rows=math.ceil(n/cols)
    fig, axes=plt.subplots(rows, cols, figsize=(8.6, rows*2.65), squeeze=False, constrained_layout=True)
    fig.patch.set_facecolor('#fbfcfe')
    for ax in axes.flat: ax.axis('off')
    for ax,(p,sub) in zip(axes.flat, present):
        ax.axis('on')
        ymin, ymax = visual_limits(p, sub['num'].tolist())
        ax.set_ylim(ymin, ymax)
        shade_reference(ax, p, ymin, ymax)
        ax.plot(sub['date'], sub['num'], marker='o', linewidth=2.2, color='#1d4ed8', zorder=3)
        ax.scatter(sub['date'], sub['num'], s=28, color='#1d4ed8', edgecolor='white', linewidth=0.7, zorder=4)
        if len(sub) == 1:
            d=sub.iloc[0]['date']
            ax.set_xlim(d - pd.Timedelta(days=45), d + pd.Timedelta(days=45))
        last=sub.iloc[-1]
        status, scol = status_for_value(p, last['num'])
        unit = '' if pd.isna(last['einheit']) else str(last['einheit']).strip()
        ax.set_title(f'{p}\naktuell {last["wert"]} {unit} · {last["date"].strftime("%d.%m.%y")} · {status}', color='#0f172a', pad=8)
        ax.grid(True, alpha=.23, color='#64748b', linewidth=.5)
        ax.tick_params(axis='x', labelrotation=25, labelsize=7.5)
        ax.tick_params(axis='y', labelsize=7.5)
        ax.text(0.01, 0.02, 'grün=Ziel/Norm · orange=grenzwertig · rot=ausserhalb', transform=ax.transAxes, fontsize=6.8, color='#475569', va='bottom', ha='left', bbox=dict(facecolor='white', alpha=.72, edgecolor='none', pad=1.8))
    out=IMGDIR/f'{fname}.png'
    fig.savefig(out, bbox_inches='tight')
    plt.close(fig)
    chart_paths.append(str(out))

# Aktuelle Wertetabelle mit Ampelstatus ergänzen
if not current.empty:
    current['Status'] = current.apply(lambda r: status_for_value(r['Parameter'], parse_num(r['Aktuell']))[0], axis=1)

# Simple sparkline PNGs for table not necessary.
styles=getSampleStyleSheet()
styles.add(ParagraphStyle(name='Small', parent=styles['BodyText'], fontSize=7.5, leading=9))
styles.add(ParagraphStyle(name='SmallWhite', parent=styles['BodyText'], fontSize=7.5, leading=9, textColor=colors.white))
styles.add(ParagraphStyle(name='BodyWhite', parent=styles['BodyText'], fontSize=9, leading=11, textColor=colors.white))
styles.add(ParagraphStyle(name='Tiny', parent=styles['BodyText'], fontSize=6.4, leading=7.5))
styles.add(ParagraphStyle(name='H1Blue', parent=styles['Title'], textColor=colors.HexColor('#0f3761'), fontSize=20, leading=24))
styles.add(ParagraphStyle(name='H2Blue', parent=styles['Heading2'], textColor=colors.HexColor('#0f3761'), fontSize=14, leading=17, spaceBefore=10, spaceAfter=6))
styles.add(ParagraphStyle(name='Box', parent=styles['BodyText'], backColor=colors.HexColor('#eef6ff'), borderColor=colors.HexColor('#93c5fd'), borderWidth=0.5, borderPadding=7, leading=11))

def P(txt, style='BodyText'):
    return Paragraph(str(txt).replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('\n','<br/>'), styles[style])

def make_table(data, widths=None, header=True, small=True, color_status=False):
    processed=[]
    for i,row in enumerate(data):
        row_style = ('SmallWhite' if small else 'BodyWhite') if (header and i == 0) else ('Small' if small else 'BodyText')
        processed.append([P(c, row_style) for c in row])
    t=Table(processed, colWidths=widths, repeatRows=1 if header else 0, hAlign='LEFT')
    style=[('VALIGN',(0,0),(-1,-1),'TOP'), ('GRID',(0,0),(-1,-1),0.25,colors.HexColor('#d7dee8')), ('LEFTPADDING',(0,0),(-1,-1),4), ('RIGHTPADDING',(0,0),(-1,-1),4), ('TOPPADDING',(0,0),(-1,-1),4), ('BOTTOMPADDING',(0,0),(-1,-1),4)]
    if header:
        style += [('BACKGROUND',(0,0),(-1,0),colors.HexColor('#0f3761')), ('TEXTCOLOR',(0,0),(-1,0),colors.white)]
    for r in range(1 if header else 0, len(data)):
        if r%2==0: style.append(('BACKGROUND',(0,r),(-1,r),colors.HexColor('#f8fafc')))
        if color_status and len(data[r]) >= 6:
            status=str(data[r][5])
            if 'Norm' in status or 'OK' in status:
                style.append(('BACKGROUND',(5,r),(5,r),colors.HexColor('#dcfce7')))
            elif 'erhöht' in status or 'niedrig' in status:
                style.append(('BACKGROUND',(5,r),(5,r),colors.HexColor('#ffedd5')))
    t.setStyle(TableStyle(style))
    return t

story=[]
story.append(P('Hyrimoz / Adalimumab bei vascular Behçet — Labor- & Urin-Kontrolle', 'H1Blue'))
story.append(P(f'Gesprächsgrundlage für den nächsten Arzttermin · erstellt am {datetime.now().strftime("%d.%m.%Y %H:%M")} · Datenquelle: {DB} · Verlauf ab 01.01.2024 · aktuelle HealthManager-DB', 'Small'))
story.append(Spacer(1,0.25*cm))
story.append(P('Kurzfazit', 'H2Blue'))
story.append(P('Priorität haben: Entzündungsaktivität (CRP + BSG), komplettes Blutbild mit Differential, Leber- und Nierenwerte, Urinstatus/Sediment plus Albumin-/Protein-Kreatinin-Ratio, sowie infektiologische Sicherheitschecks vor/unter TNF-Hemmer (TB/IGRA, HBV/HCV/HIV). Bei vascular Behçet ist das Ziel nicht “Laborkosmetik”, sondern objektive Krankheitskontrolle plus frühzeitige Erkennung von Infekt-, Leber-, Nieren- oder Blutungsproblemen.', 'Box'))
story.append(Spacer(1,0.2*cm))

story.append(P('1) Empfohlene Kontrollen mit Begründung', 'H2Blue'))
rec_data=[['Priorität','Wert/Panel','Zweck','Begründung für Arztgespräch','Quellen']]
for r in RECOMMENDATIONS:
    rec_data.append(list(r))
story.append(make_table(rec_data, widths=[1.7*cm,3.4*cm,3.0*cm,6.3*cm,3.7*cm], small=True))
story.append(Spacer(1,0.25*cm))

story.append(P('2) Aktuelle Werte aus der Gesundheitsdatenbank', 'H2Blue'))
cur_data=[list(current.columns)] + current.fillna('').astype(str).values.tolist()
story.append(make_table(cur_data, widths=[3.4*cm,2.5*cm,2.0*cm,2.0*cm,6.1*cm,2.0*cm], small=True, color_status=True))

story.append(PageBreak())
story.append(P('3) Verlaufsgrafiken', 'H2Blue'))
story.append(P('Hinweis: Es werden nur Messwerte ab 01.01.2024 dargestellt. Einige Daten wurden aus Dokumenten nachträglich importiert; wenn die Bemerkung ein Messdatum enthielt, wurde dieses Messdatum verwendet. Grün/orange/rot sind visuelle Orientierungsbereiche; labor- und arztseitige Referenzbereiche bleiben verbindlich.', 'Small'))
for cp in chart_paths:
    story.append(Spacer(1,0.15*cm))
    story.append(Image(cp, width=18.2*cm, height=0))

story.append(PageBreak())
story.append(P('4) Konkrete Fragen an die Ärztin / den Arzt', 'H2Blue'))
questions=[
['Thema','Frage'],
['Therapieziel','Welche objektiven Kriterien zeigen bei mir, dass Hyrimoz wirkt? CRP/BSG, Symptome, Duplex/MRI, keine neuen SVT/Thrombosen?'],
['Monitoring-Frequenz','Wie oft Blutbild, Leber, Niere, CRP/BSG und Urin in den ersten 3–6 Monaten?'],
['Urin/Niere','Soll wegen Behçet/Eliquis regelmässig Urinstatus + Albumin/Kreatinin + Protein/Kreatinin kontrolliert werden?'],
['Infektionsscreening','Ist IGRA/Quantiferon dokumentiert? HBV/HCV/HIV liegen teilweise vor — reicht das oder braucht es Wiederholung vor Start?'],
['Antikoagulation','Welche Kriterien müssten erfüllt sein, bevor Eliquis irgendwann überhaupt diskutiert/reduziert würde?'],
['Lipide/CV-Risiko','LDL/non-HDL waren erhöht: Soll vascular Behçet als zusätzlicher Risikofaktor in die Lipidstrategie einfliessen?'],
['Alarmplan','Bei welchen Symptomen soll Hyrimoz pausiert und sofort Kontakt aufgenommen werden? Fieber, Infekt, Atemnot, neue neurologische Zeichen, starke Hautinfektion?'],
]
story.append(make_table(questions, widths=[4.0*cm,14.0*cm], small=False))

story.append(P('5) Quellen', 'H2Blue'))
source_data=[['Kürzel','Referenz']]+[[k,v] for k,v in SOURCES.items()]
story.append(make_table(source_data, widths=[3.1*cm,14.8*cm], small=True))
story.append(Spacer(1,0.2*cm))
story.append(P('Medizinischer Hinweis: Dieser Bericht ersetzt keine ärztliche Beurteilung. Er dient als strukturierte Gesprächsgrundlage und Priorisierung der Laborkontrollen.', 'Small'))

def add_page_number(canvas, doc):
    canvas.saveState()
    canvas.setFont('Helvetica', 8)
    canvas.setFillColor(colors.HexColor('#64748b'))
    canvas.drawRightString(A4[0]-1.4*cm, 0.8*cm, f'Seite {doc.page}')
    canvas.drawString(1.4*cm, 0.8*cm, 'JARVIS Health · Hyrimoz/Behçet Monitoring')
    canvas.restoreState()

# fix Image proportional height after story built: reportlab Image height=0 may fail; set manually based on aspect ratio
for idx,el in enumerate(story):
    if isinstance(el, Image):
        from PIL import Image as PILImage
        im=PILImage.open(el.filename)
        w=18.2*cm; h=w*im.height/im.width
        el.drawWidth=w; el.drawHeight=h

doc=SimpleDocTemplate(PDF, pagesize=A4, rightMargin=1.2*cm, leftMargin=1.2*cm, topMargin=1.25*cm, bottomMargin=1.25*cm)
doc.build(story, onFirstPage=add_page_number, onLaterPages=add_page_number)

# HTML sibling for browser review
html_parts=[f'''<!doctype html><html><head><meta charset="utf-8"><title>Hyrimoz Laborkontrolle</title><style>
body{{font-family:Inter,Arial,sans-serif;background:#f6f8fb;color:#0f172a;margin:0;padding:32px}} .page{{max-width:1100px;margin:auto;background:white;padding:32px;border-radius:18px;box-shadow:0 8px 28px #0001}} h1{{color:#0f3761}} h2{{color:#0f3761;border-bottom:2px solid #dbeafe;padding-bottom:6px}} table{{border-collapse:collapse;width:100%;font-size:13px;margin:12px 0}} th{{background:#0f3761;color:white;text-align:left}} td,th{{border:1px solid #d7dee8;padding:7px;vertical-align:top}} tr:nth-child(even){{background:#f8fafc}} .box{{background:#eef6ff;border-left:5px solid #2563eb;padding:14px;border-radius:10px}} img{{max-width:100%;border:1px solid #e2e8f0;border-radius:12px;margin:10px 0}}
</style></head><body><div class="page"><h1>Hyrimoz / Adalimumab bei vascular Behçet — Labor- & Urin-Kontrolle</h1><p>Erstellt {datetime.now().strftime('%d.%m.%Y %H:%M')} · Datenquelle: {html.escape(DB)}</p><div class="box">Kurzfazit: Priorität haben CRP+BSG, Blutbild mit Differential, Leber/Niere, Urinstatus/Sediment plus Albumin-/Protein-Kreatinin-Ratio sowie TB/HBV/HCV/HIV-Sicherheitschecks.</div>''']
# add tables simple
for title, df in [('Empfohlene Kontrollen', pd.DataFrame(RECOMMENDATIONS, columns=['Priorität','Wert/Panel','Zweck','Begründung','Quellen'])), ('Aktuelle Werte', current)]:
    html_parts.append(f'<h2>{title}</h2>'+df.to_html(index=False, escape=True))
html_parts.append('<h2>Verlaufsgrafiken</h2>')
for cp in chart_paths:
    html_parts.append(f'<img src="charts/{Path(cp).name}">')
html_parts.append('<h2>Quellen</h2>'+pd.DataFrame(list(SOURCES.items()), columns=['Kürzel','Referenz']).to_html(index=False, escape=True))
html_parts.append('</div></body></html>')
Path(HTML).write_text('\n'.join(html_parts), encoding='utf-8')

print('PDF', PDF)
print('HTML', HTML)
print('CHARTS', len(chart_paths))
print('CURRENT_ROWS', len(current))
print(current.to_string(index=False))
