// PipelineView.jsx – ADPKD Clinical Patient Journey
// Top-to-bottom flow · branches · loop-back · rich node details · Agent chat · Deep Dive
const { useState: usePV, useRef: useRefPV, useEffect: useEffectPV } = React;

// ── Canvas geometry ──────────────────────────────────────────────────────────
const CW = 1180, CH = 1320;
const LANE_X = 58;
const LANE_W = 960;

// ── Phases (swim-lane bands) ─────────────────────────────────────────────────
const PHASES = [
  { id:'pre',   short:'Pre-Clinic',              color:'#7A6545', bg:'#FAF8F4', y:40,   h:220 },
  { id:'visit', short:'Initial Clinic Visit',    color:'#2D5A8E', bg:'#F4F7FC', y:268,  h:120 },
  { id:'diag',  short:'Diagnostic Process',      color:'#3D7A5E', bg:'#F4FAF7', y:396,  h:200 },
  { id:'tdec',  short:'Treatment Decision',      color:'#6B5FA6', bg:'#F7F5FC', y:604,  h:120 },
  { id:'impl',  short:'Treatment Implementation',color:'#C77700', bg:'#FFFBF3', y:732,  h:260 },
  { id:'adv',   short:'Advanced Care',           color:'#D32027', bg:'#FFF5F5', y:1000, h:280 },
];

// ── Nodes ────────────────────────────────────────────────────────────────────
// Each node carries:
//   meta          - rich, structured details for the side panel
//   guideline     - primary clinical guideline reference
//   dataSources   - which MeDIC-harmonised sources feed this step
//   cohort        - how many of n=312 pass through, with median dwell time
//   keyMetrics    - metrics surfaced for Selected Patient view
const NODES = [
  { id:'e1', phase:'pre', label:['Symptom','self-referral'], x:180, y:80, w:150, h:52, entry:true,
    desc:'Patient, family or GP recognises flank pain, hematuria, or unexplained hypertension.',
    meta:{
      triggers:['Flank / abdominal pain','Gross hematuria','Hypertension (<40 yr)','Recurrent UTIs','Nephrolithiasis'],
      prevalence:'ADPKD accounts for ~10% of end-stage kidney disease worldwide (1:400–1:1000 live births).',
      referralPath:'GP → nephrology outpatient (avg wait: 23 days, UKK)',
    },
    guideline:'KDIGO 2025 §2.1 · case definition', dataSources:['pghd','ehr'],
    cohort:{ n:212, pct:68, dwellMedian:'23 d' },
    keyMetrics:[{k:'Symptom onset → referral', v:'median 42 d'},{k:'Age at first symptom', v:'median 36 yr'}],
  },
  { id:'e2', phase:'pre', label:['Family','screening'], x:480, y:80, w:150, h:52, entry:true,
    desc:'First-degree relative of known ADPKD patient enters proactive screening.',
    meta:{
      triggers:['Affected parent','Proband-triggered cascade screening','Pre-conception counseling'],
      guidanceNote:'Imaging screening typically from age 18 per KDIGO; earlier if symptomatic.',
    },
    guideline:'KDIGO 2025 §3.2 · at-risk family members', dataSources:['ehr','genetics','registry'],
    cohort:{ n:56, pct:18, dwellMedian:'—' },
    keyMetrics:[{k:'Age at screening', v:'median 22 yr'},{k:'Pedigree depth', v:'≥2 generations'}],
  },
  { id:'e3', phase:'pre', label:['Incidental','finding'], x:780, y:80, w:150, h:52, entry:true,
    desc:'Bilateral cysts incidentally discovered on imaging ordered for unrelated indication.',
    meta:{
      triggers:['Abdominal US for other indication','CT trauma/oncology','MRI abdomen'],
      imagingYield:'Positive predictive value of ≥3 cysts bilaterally in at-risk adult: 97%',
    },
    guideline:'Pei-Ravine ultrasound criteria · 2009', dataSources:['pacs','ehr'],
    cohort:{ n:44, pct:14, dwellMedian:'—' },
    keyMetrics:[{k:'Cysts at discovery', v:'median 6'}],
  },
  { id:'s2', phase:'pre', label:['Find','Specialist'], x:480, y:186, w:150, h:52,
    desc:'Selecting a clinic with ADPKD expertise — referral to UKK Nephrology outpatient.',
    meta:{
      actions:['Referral submission','Insurance authorisation','Clinic matching'],
      outcomes:['First appointment scheduled','Records transfer initiated'],
    },
    guideline:'UKK Nephrology SOP-N-04 · referral workflow',
    dataSources:['ehr','insurance'],
    cohort:{ n:312, pct:100, dwellMedian:'14 d' },
    keyMetrics:[{k:'Referral → 1st visit', v:'median 14 d'},{k:'No-show rate', v:'6%'}],
  },
  { id:'s4', phase:'visit', label:['Arrival &','Registration'], x:330, y:308, w:150, h:52,
    desc:'Administrative onboarding, medical history intake, prior records ingestion.',
    meta:{
      actions:['Demographics capture','Consent (treatment + DICE-CD research)','History intake','Prior imaging import'],
      forms:['MII-Kerndatensatz basics','Research consent v3.1'],
    },
    guideline:'UKK SOP-A-02 · admission + MII consent',
    dataSources:['ehr','registry'],
    cohort:{ n:312, pct:100, dwellMedian:'same-day' },
    keyMetrics:[{k:'Consent uptake (DICE-CD)', v:'88%'}],
  },
  { id:'s5', phase:'visit', label:['Initial','Assessment'], x:630, y:308, w:150, h:52,
    desc:'Physical exam, BP, family history, risk stratification, triaging of diagnostics.',
    meta:{
      actions:['Vital signs + BMI','Bilateral flank exam','Pedigree documentation','Risk stratification (Mayo Class pre-scoring)'],
      decisions:['Diagnostic panel selection','Genetic counseling referral (conditional)'],
    },
    guideline:'KDIGO 2025 §4.1 · initial evaluation',
    dataSources:['ehr','registry','pghd'],
    cohort:{ n:312, pct:100, dwellMedian:'45 min' },
    keyMetrics:[{k:'BP at first visit (%≥140/90)', v:'61%'},{k:'Pedigree captured', v:'94%'}],
  },
  { id:'s6', phase:'diag', label:['Laboratory','Tests'], x:180, y:438, w:150, h:52,
    desc:'Core renal biochemistry, hepatic function, urinalysis — establish CKD stage.',
    meta:{
      panels:['Serum creatinine + cystatin C → eGFR (CKD-EPI 2021)','LFT baseline (pre-Tolvaptan)','Urinalysis + albumin/creatinine ratio','Electrolytes + copeptin'],
      frequency:'Baseline + every 3–6 months per CKD stage',
    },
    guideline:'KDIGO 2025 §5.2 · lab panel',
    dataSources:['lis','ehr'],
    cohort:{ n:312, pct:100, dwellMedian:'2 d turnaround' },
    keyMetrics:[{k:'eGFR at baseline', v:'median 64 ml/min/1.73m²'},{k:'UACR elevated', v:'34%'}],
  },
  { id:'s7', phase:'diag', label:['Ultrasound'], x:480, y:438, w:150, h:52,
    desc:'First-line renal ultrasound — Pei-Ravine criteria; operator-dependent.',
    meta:{
      findings:['Cyst count (bilateral, age-stratified)','Kidney length','Parenchymal thinning','Stones / hydronephrosis'],
      criteria:'Pei 2009: ≥3 cysts bilaterally (age 15–39), ≥2 per kidney (40–59), ≥4 per kidney (≥60)',
      limitations:'TKV not reliably quantifiable; progression tracking limited',
    },
    guideline:'Pei-Ravine US criteria',
    dataSources:['pacs','ehr'],
    cohort:{ n:300, pct:96, dwellMedian:'same-day' },
    keyMetrics:[{k:'Cyst count', v:'median 14'},{k:'Kidney length (L)', v:'median 13.8 cm'}],
  },
  { id:'s7b', phase:'diag', label:['MRI / CT','+ AI-TKV'], x:480, y:524, w:150, h:52, anchor:true,
    desc:'Detailed volumetry; deep-learning total kidney volume segmentation. MRI preferred when eGFR reduced.',
    meta:{
      protocols:['T2 HASTE coronal/axial','Single-shot TKV sequence (~8 min)','No gadolinium (3D-SPACE)'],
      aiOutputs:['TKV (ml)','htTKV (ml/m)','Mayo Imaging Class 1A–1E','Growth rate (ml/yr) vs prior scan'],
      validation:'Dice 0.96 vs nephroradiologist consensus · n=148 internal cohort',
    },
    guideline:'Irazabal / Mayo Imaging Classification 2015',
    dataSources:['pacs','medic'],
    cohort:{ n:141, pct:45, dwellMedian:'7 d' },
    keyMetrics:[{k:'htTKV', v:'median 780 ml/m'},{k:'Mayo class', v:'mode 1C'},{k:'Growth rate', v:'median 5.2% /yr'}],
    innovation:'DICE-CD Anchor · AI-TKV segmentation pipeline',
  },
  { id:'s8', phase:'diag', label:['Genetic','Counseling'], x:780, y:438, w:150, h:52, optional:true,
    desc:'PKD1 / PKD2 sequencing, ACMG classification; informs prognosis + family cascade screening.',
    meta:{
      panels:['PKD1 full gene NGS (pseudogene-aware)','PKD2 NGS','GANAB / DNAJB11 on atypical cases','Copy-number analysis'],
      actions:['Pre-test counseling','Sample collection','ACMG 5-tier classification','Post-test counseling + pedigree update'],
      yield:'Pathogenic variant detected in 92% when family history positive',
    },
    guideline:'ACMG 2015 · KDIGO 2025 §6 genetic testing',
    dataSources:['genetics','ehr'],
    cohort:{ n:172, pct:55, dwellMedian:'21 d' },
    keyMetrics:[{k:'Variant detection', v:'89%'},{k:'PKD1 truncating', v:'42%'},{k:'VUS rate', v:'7%'}],
  },
  { id:'s9', phase:'tdec', label:['Nephrologist','Consultation'], x:330, y:644, w:150, h:52,
    desc:'Diagnostic review, confirmation of diagnosis, CKD staging, risk communication.',
    meta:{
      actions:['Diagnosis confirmation','CKD stage assignment','Risk tier assignment (rapid progressor?)','Patient education + decision aid'],
      outputs:['Structured clinical summary (FHIR Composition)','Risk letter for patient'],
    },
    guideline:'KDIGO 2025 §7 · communicating risk',
    dataSources:['ehr','medic','lis','pacs','genetics'],
    cohort:{ n:312, pct:100, dwellMedian:'45 min' },
    keyMetrics:[{k:'Rapid-progressor rate', v:'31%'},{k:'Decision aid used', v:'74%'}],
  },
  { id:'s10', phase:'tdec', label:['Treatment','Planning (MDT)'], x:630, y:644, w:150, h:52,
    desc:'Multidisciplinary plan: CKD stage, genetics, Mayo class, patient preferences.',
    meta:{
      participants:['Nephrologist','Clinical pharmacist','Genetic counselor','Radiologist (on-call)','Patient + caregiver'],
      decisions:['Tolvaptan eligibility','BP target + agent','Monitoring cadence','Trial matching'],
    },
    guideline:'KDIGO 2025 §8.3 · shared decision-making',
    dataSources:['ehr','medic','pharmacy','trials','registry'],
    cohort:{ n:312, pct:100, dwellMedian:'5 d' },
    keyMetrics:[{k:'Plan documented in FHIR CarePlan', v:'100%'},{k:'Trial-eligible', v:'18%'}],
  },
  { id:'s11', phase:'impl', label:['Hypertension','Management'], x:82, y:780, w:150, h:52,
    desc:'ACE-i / ARBs as first-line; lifestyle; home BP monitoring (HALT-PKD evidence).',
    meta:{
      targets:['BP <110/75 mmHg for eGFR >60 + htTKV > mean (HALT-PKD A)','BP <130/80 standard'],
      agents:['Lisinopril / Ramipril (first-line)','Telmisartan (if ACE-i intolerant)'],
      monitoring:'Home BP 2× daily × 7 days, quarterly clinic review',
    },
    guideline:'HALT-PKD + KDIGO 2025 §9.1',
    dataSources:['ehr','pghd','pharmacy'],
    cohort:{ n:243, pct:78, dwellMedian:'ongoing' },
    keyMetrics:[{k:'BP at target', v:'62%'},{k:'Home BP adherence', v:'51%'}],
  },
  { id:'s12', phase:'impl', label:['Symptom','Management'], x:302, y:780, w:150, h:52,
    desc:'Pain control, UTI / cyst infection management, nephrolithiasis, hematuria.',
    meta:{
      domains:['Chronic flank pain (stepwise analgesia, avoid NSAIDs)','Acute cyst infection (lipophilic abx)','Recurrent UTI prophylaxis','Stone prevention'],
      redFlags:['Persistent fever despite abx','Gross hematuria >48h','Severe acute pain'],
    },
    guideline:'UKK Nephrology SOP-P-06',
    dataSources:['ehr','pharmacy','lis'],
    cohort:{ n:131, pct:42, dwellMedian:'variable' },
    keyMetrics:[{k:'Pain score reduction (NRS)', v:'median −3 pts'}],
  },
  { id:'s13', phase:'impl', label:['Lifestyle','Modifications'], x:522, y:780, w:150, h:52,
    desc:'Sodium <2 g/day, water ≥3 L/day (if tolerated), protein moderation, exercise.',
    meta:{
      interventions:['Dietitian consultation','Water intake app (PGHD)','Sodium tracking','Structured exercise plan'],
      evidence:'High water intake may suppress vasopressin; inconsistent RCT evidence, individualised',
    },
    guideline:'KDIGO 2025 §10 · lifestyle + diet',
    dataSources:['pghd','apps','ehr'],
    cohort:{ n:287, pct:92, dwellMedian:'ongoing' },
    keyMetrics:[{k:'Water intake ≥3 L', v:'49%'},{k:'Sodium <2 g', v:'38%'}],
  },
  { id:'s14', phase:'impl', label:['Tolvaptan'], x:742, y:780, w:150, h:52, anchor:true,
    desc:'Vasopressin V2-receptor antagonist · under strict eligibility · mandatory monthly LFT.',
    meta:{
      eligibility:['Age 18–55','Mayo Class 1C/1D/1E OR rapid progression','eGFR ≥25 ml/min/1.73m²','Adequate water access'],
      contraindications:['Hepatic impairment','Prior hepatotoxicity','Inability to sense thirst','Pregnancy / breastfeeding'],
      safety:['Monthly LFT × 18 mo, then 3-monthly','REMS hepatotoxicity program','Hydration counseling'],
    },
    guideline:'TEMPO 3:4 + REPRISE · EMA/FDA label',
    dataSources:['ehr','pharmacy','lis','medic'],
    cohort:{ n:69, pct:22, dwellMedian:'ongoing' },
    keyMetrics:[{k:'Currently on Tolvaptan', v:'22%'},{k:'LFT flag rate', v:'4.2%'},{k:'Discontinuation', v:'11%'}],
    innovation:'DICE-CD Anchor · automated LFT surveillance + adherence signal',
  },
  { id:'s15', phase:'impl', label:['Regular','Monitoring'], x:412, y:910, w:200, h:62, anchor:true, hub:true,
    desc:'Kidney function trajectory + AI-supported volume progression. Core DICE-CD anchor.',
    meta:{
      cadence:['eGFR + UACR every 3–6 mo','MRI-TKV every 12 mo (18 mo if stable)','BP home monitoring continuous','Tolvaptan LFT per schedule'],
      riskSignals:['eGFR drop >5 ml/min/yr','htTKV growth >6% /yr','Mayo class shift upward','BP excursions >30%'],
      aiOutputs:['Trajectory forecast (5 yr)','Peer-group percentile','Alert triage'],
    },
    guideline:'UKK Nephrology SOP-M-12 · longitudinal monitoring',
    dataSources:['medic','lis','pacs','pghd','ehr'],
    cohort:{ n:312, pct:100, dwellMedian:'indefinite' },
    keyMetrics:[{k:'Trajectory deviation flags', v:'17% /yr'},{k:'AI-TKV scans /patient', v:'median 3'}],
    innovation:'DICE-CD Anchor · unified longitudinal trajectory + alerting',
  },
  { id:'t1', phase:'adv', label:['Stable','(long-term)'], x:180, y:1060, w:150, h:52, terminal:true,
    desc:'Stable on monitoring + supportive care. Longitudinal nephrology follow-up continues.',
    meta:{
      criteria:['eGFR decline <2 ml/min/yr','Stable Mayo class','No red-flag events in 24 mo'],
      cadence:'Clinic review every 6–12 mo',
    },
    guideline:'UKK SOP-M-14 · stable-cohort management',
    dataSources:['ehr','lis'],
    cohort:{ n:193, pct:62, dwellMedian:'≥5 yr' },
    keyMetrics:[{k:'Stable ≥5 yr', v:'62%'}],
  },
  { id:'a1', phase:'adv', label:['Kidney Failure','(ESRD)'], x:480, y:1060, w:150, h:52,
    desc:'Progression to end-stage — evaluation for dialysis modality or pre-emptive transplant.',
    meta:{
      triggers:['eGFR <15 ml/min/1.73m²','Uremic symptoms','Fluid / electrolyte instability'],
      actions:['Modality education','Access planning (AVF/PD catheter)','Transplant workup referral'],
    },
    guideline:'KDIGO 2025 §12 · RRT preparation',
    dataSources:['ehr','lis','registry'],
    cohort:{ n:56, pct:18, dwellMedian:'variable' },
    keyMetrics:[{k:'Median age at ESRD', v:'58 yr'},{k:'Pre-emptive RRT planned', v:'43%'}],
  },
  { id:'a2', phase:'adv', label:['Dialysis'], x:380, y:1170, w:150, h:44, terminal:true,
    desc:'HD / PD initiation based on modality choice, access readiness, lifestyle.',
    meta:{
      modalities:['In-centre HD (3× /wk)','Home HD','Peritoneal dialysis (CAPD/APD)'],
    },
    guideline:'KDIGO 2025 §12.2',
    dataSources:['ehr','registry','insurance'],
    cohort:{ n:30, pct:10, dwellMedian:'indefinite' },
    keyMetrics:[{k:'HD : PD split', v:'64:36'}],
  },
  { id:'a3', phase:'adv', label:['Transplant','Evaluation'], x:580, y:1170, w:150, h:44, terminal:true,
    desc:'Full workup + listing / living-donor pathway.',
    meta:{
      workup:['Tissue typing + PRA','Cardiac + infectious workup','Psychosocial evaluation','Living donor eval (if available)'],
    },
    guideline:'Eurotransplant + UKK Transplant SOP',
    dataSources:['ehr','registry','genetics'],
    cohort:{ n:26, pct:8, dwellMedian:'listing ~18 mo' },
    keyMetrics:[{k:'Living-donor match rate', v:'27%'}],
  },
  { id:'c1', phase:'adv', label:['Ongoing Support','& Counseling'], x:780, y:1060, w:160, h:52,
    desc:'Psychosocial support, patient education, social care navigation.',
    meta:{
      services:['ADPKD patient group (UKK)','Psycho-nephrology clinic','Social worker referrals','Family genetic counseling'],
    },
    guideline:'UKK Support Services',
    dataSources:['ehr','registry','social'],
    cohort:{ n:110, pct:35, dwellMedian:'ongoing' },
    keyMetrics:[{k:'Patient group enrollment', v:'35%'}],
  },
];

// ── Edges ────────────────────────────────────────────────────────────────────
const EDGES = [
  { f:'e1', t:'s2', p:68, rationale:'Symptom-driven referral is the dominant entry route in the UKK cohort.' },
  { f:'e2', t:'s2', p:18, rationale:'Cascade screening of first-degree relatives.' },
  { f:'e3', t:'s2', p:14, rationale:'Incidental cysts on abdominal imaging for unrelated indication.' },
  { f:'s2', t:'s4', p:100 },
  { f:'s4', t:'s5', p:100 },
  { f:'s5', t:'s6', p:100 },
  { f:'s5', t:'s7', p:96 },
  { f:'s5', t:'s8', p:55, optional:true, rationale:'Genetic counselling offered when family history positive or diagnosis equivocal.' },
  { f:'s7', t:'s7b', p:45, branch:true, label:'further assessment', subtle:true, rationale:'Ultrasound triggers MRI when Mayo class uncertain or TKV tracking needed.' },
  { f:'s6', t:'s9', p:100 },
  { f:'s7', t:'s9', p:100, hidden:true },
  { f:'s7b', t:'s9', p:100 },
  { f:'s8', t:'s9', p:100 },
  { f:'s9', t:'s10', p:100 },
  { f:'s10', t:'s11', p:78, branch:true, rationale:'Hypertension present in the majority at treatment planning.' },
  { f:'s10', t:'s12', p:42, branch:true, rationale:'Symptomatic care triggered by pain, infection, or hematuria.' },
  { f:'s10', t:'s13', p:92, branch:true, rationale:'Lifestyle plan issued to nearly all patients.' },
  { f:'s10', t:'s14', p:22, branch:true, anchor:true, rationale:'Tolvaptan initiated only in eligible rapid-progressors.' },
  { f:'s11', t:'s15', p:100 },
  { f:'s12', t:'s15', p:100 },
  { f:'s13', t:'s15', p:100 },
  { f:'s14', t:'s15', p:100 },
  { f:'s15', t:'t1', p:62, branch:true, rationale:'Majority remain in stable long-term monitoring.' },
  { f:'s15', t:'s10', p:35, branch:true, loopback:true, label:'plan revision', rationale:'Regimen revised on intolerance, progression, or new genetic / imaging data.' },
  { f:'s15', t:'a1', p:18, branch:true, rationale:'Progression to ESRD over observation window.' },
  { f:'s15', t:'c1', p:35, branch:true, rationale:'Referral to support services as disease and psychosocial burden grows.' },
  { f:'a1', t:'a2', p:54, branch:true },
  { f:'a1', t:'a3', p:46, branch:true },
];

const DOMINANT_PATH = ['e1','s2','s4','s5','s6','s9','s10','s13','s15','t1'];
const PEER_DELTAS = {'e1-s2':-4,'e2-s2':+7,'s5-s8':+18,'s10-s14':+12,'s15-t1':-8,'s15-s10':+3,'s15-a1':+5};

// Per-patient traversed / forecast edges
function getPatientEdges(patient) {
  if (!patient) return { traversed:[], forecast:[] };
  const t = ['e1-s2','s2-s4','s4-s5','s5-s6','s5-s7','s6-s9','s9-s10'];
  const f = [];
  if (patient.pkdVariant) t.push('s5-s8');
  if (patient.mayoClass && ['1C','1D','1E'].includes(patient.mayoClass)) t.push('s7-s7b','s7b-s9');
  else t.push('s7-s9');
  if (patient.decision==='Eligible') { t.push('s10-s14','s10-s11','s10-s13'); f.push('s14-s15','s11-s15','s13-s15','s15-t1'); }
  else if (patient.decision==='Review') { t.push('s10-s11','s10-s13'); f.push('s11-s15','s13-s15'); }
  else { t.push('s10-s11','s10-s13','s11-s15','s13-s15'); f.push('s15-t1'); }
  return { traversed:t, forecast:f };
}

// Data source label dictionary (short names for chips)
const DS_LABELS = {
  ehr:'EHR', lis:'LIS', pacs:'PACS', pharmacy:'Pharmacy', genetics:'Genetics',
  registry:'Registry', medic:'MeDIC', pghd:'PGHD', apps:'Health Apps',
  ehis:'Public Health', trials:'Clinical Trials', insurance:'Insurance', social:'Social Media',
};

// ── Utilities ────────────────────────────────────────────────────────────────
function nodeById(id) { return NODES.find(n => n.id === id); }
function edgeKey(e) { return `${e.f}-${e.t}`; }
function routeEdge(edge) {
  const f = nodeById(edge.f), t = nodeById(edge.t);
  if (!f || !t) return null;
  if (edge.loopback) {
    const fx = f.x + f.w, fy = f.y + f.h/2;
    const tx = t.x + t.w, ty = t.y + t.h/2;
    const outX = CW - 30;
    return `M ${fx} ${fy} L ${outX} ${fy} L ${outX} ${ty} L ${tx} ${ty}`;
  }
  const fx = f.x + f.w/2, fy = f.y + f.h;
  const tx = t.x + t.w/2, ty = t.y;
  if (Math.abs(fy - (t.y + t.h)) < 4) return `M ${f.x + f.w} ${f.y + f.h/2} L ${t.x} ${t.y + t.h/2}`;
  const my = fy + (ty - fy) * 0.5;
  return `M ${fx} ${fy} C ${fx} ${my}, ${tx} ${my}, ${tx} ${ty}`;
}
function midpointOfPath(edge) {
  const f = nodeById(edge.f), t = nodeById(edge.t);
  if (edge.loopback) return { x: CW - 30, y: (f.y + t.y)/2 + 30 };
  return { x: (f.x + f.w/2 + t.x + t.w/2)/2, y: (f.y + f.h + t.y)/2 };
}

// ── Lightweight Markdown renderer (safe subset) ─────────────────────────────
// Supports: **bold**, *italic*, `code`, [text](url), headings (#/##/###),
// bullet lists (-, *, •), numbered lists (1. 2.), paragraphs, line breaks.
function renderInline(text, keyPrefix='') {
  const out = [];
  const pattern = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\([^)]+\))/g;
  let last = 0, m, idx = 0;
  while ((m = pattern.exec(text)) !== null) {
    if (m.index > last) out.push(text.slice(last, m.index));
    const tok = m[0];
    const k = `${keyPrefix}-${idx++}`;
    if (tok.startsWith('**')) out.push(<strong key={k}>{tok.slice(2,-2)}</strong>);
    else if (tok.startsWith('`')) out.push(<code key={k} style={{background:'#F2F3F5', padding:'1px 5px', borderRadius:3, fontFamily:'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize:11}}>{tok.slice(1,-1)}</code>);
    else if (tok.startsWith('[')) {
      const mm = /\[([^\]]+)\]\(([^)]+)\)/.exec(tok);
      out.push(<a key={k} href={mm[2]} target="_blank" rel="noopener noreferrer" style={{color:'#D32027', textDecoration:'underline'}}>{mm[1]}</a>);
    }
    else if (tok.startsWith('*')) out.push(<em key={k}>{tok.slice(1,-1)}</em>);
    last = m.index + tok.length;
  }
  if (last < text.length) out.push(text.slice(last));
  return out;
}

function Markdown({ text }) {
  if (!text) return null;
  const lines = String(text).replace(/\r\n/g, '\n').split('\n');
  const blocks = [];
  let i = 0;
  while (i < lines.length) {
    const line = lines[i];
    // blank
    if (!line.trim()) { i++; continue; }
    // headings
    const h = /^(#{1,3})\s+(.*)$/.exec(line);
    if (h) {
      const lvl = h[1].length;
      const tag = lvl===1?'h3':lvl===2?'h4':'h5';
      const size = lvl===1?14:lvl===2?13:12;
      blocks.push(React.createElement(tag, { key:`h-${i}`, style:{fontSize:size, fontWeight:700, margin:'6px 0 4px', color:'inherit'} }, renderInline(h[2], `h${i}`)));
      i++; continue;
    }
    // unordered list
    if (/^[\s]*[-*•]\s+/.test(line)) {
      const items = [];
      while (i < lines.length && /^[\s]*[-*•]\s+/.test(lines[i])) {
        items.push(lines[i].replace(/^[\s]*[-*•]\s+/, ''));
        i++;
      }
      blocks.push(
        <ul key={`ul-${i}`} style={{margin:'4px 0', paddingLeft:16}}>
          {items.map((it, j) => <li key={j} style={{marginBottom:2}}>{renderInline(it, `ul${i}${j}`)}</li>)}
        </ul>
      );
      continue;
    }
    // ordered list
    if (/^[\s]*\d+\.\s+/.test(line)) {
      const items = [];
      while (i < lines.length && /^[\s]*\d+\.\s+/.test(lines[i])) {
        items.push(lines[i].replace(/^[\s]*\d+\.\s+/, ''));
        i++;
      }
      blocks.push(
        <ol key={`ol-${i}`} style={{margin:'4px 0', paddingLeft:20}}>
          {items.map((it, j) => <li key={j} style={{marginBottom:2}}>{renderInline(it, `ol${i}${j}`)}</li>)}
        </ol>
      );
      continue;
    }
    // paragraph (collect consecutive non-empty, non-list, non-heading lines)
    const para = [line];
    i++;
    while (i < lines.length && lines[i].trim() && !/^(#{1,3}\s|[\s]*[-*•]\s|[\s]*\d+\.\s)/.test(lines[i])) {
      para.push(lines[i]);
      i++;
    }
    blocks.push(<p key={`p-${i}`} style={{margin:'4px 0'}}>{renderInline(para.join(' '), `p${i}`)}</p>);
  }
  return <div>{blocks}</div>;
}

// ── Agent Chat ──────────────────────────────────────────────────────────────
function AgentChat({ node, patient, onClose }) {
  const [messages, setMessages] = usePV(() => {
    const greet = `I'm your clinical agent for **${node.label.join(' ')}**. I have context on ${patient?`${patient.alias}, `:''}the KDIGO guideline, the linked data sources (${(node.dataSources||[]).map(d=>DS_LABELS[d]).join(', ')}), and the cohort baseline (n=${node.cohort?.n}, ${node.cohort?.pct}% of cohort). Ask me anything about this step.`;
    return [{ role:'assistant', text:greet }];
  });
  const [input, setInput] = usePV('');
  const [busy, setBusy] = usePV(false);
  const scrollRef = useRefPV(null);
  useEffectPV(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [messages, busy]);

  const suggestions = [
    `Summarize the evidence base for this step`,
    `What data sources feed this step and their quality?`,
    patient ? `Why did ${patient.alias} arrive here?` : `What determines whether a patient arrives here?`,
    `What are the red-flag signals to watch?`,
  ];

  async function send(q) {
    const question = (q ?? input).trim();
    if (!question || busy) return;
    setInput('');
    setMessages(m => [...m, { role:'user', text: question }]);
    setBusy(true);
    try {
      const context = [
        `You are a clinical decision-support agent embedded in the DICE-CD / UKK Nephrology dashboard.`,
        `The physician is asking about the "${node.label.join(' ')}" step of the ADPKD patient journey.`,
        `Step description: ${node.desc}`,
        node.guideline ? `Primary guideline: ${node.guideline}` : '',
        node.meta ? `Structured detail: ${JSON.stringify(node.meta)}` : '',
        node.cohort ? `Cohort (n=312): ${node.cohort.n} patients (${node.cohort.pct}%), median dwell ${node.cohort.dwellMedian}` : '',
        node.keyMetrics ? `Key metrics: ${node.keyMetrics.map(m=>`${m.k}=${m.v}`).join('; ')}` : '',
        node.dataSources ? `Data sources fed through MeDIC: ${node.dataSources.map(d=>DS_LABELS[d]).join(', ')}` : '',
        patient ? `Selected patient: ${patient.alias}, age ${patient.age}, sex ${patient.sex}, eGFR ${patient.egfr||'n/a'}, Mayo class ${patient.mayoClass||'n/a'}, decision ${patient.decision||'n/a'}, risk tier ${patient.riskTier||'n/a'}` : 'No patient selected.',
        `Answer in 3–6 concise sentences. Be specific, cite the guideline where relevant, and use bullet points when listing. Do not invent data values — if unknown, say so.`,
      ].filter(Boolean).join('\n\n');
      const answer = await window.claude.complete({
        messages: [{ role:'user', content: `${context}\n\nPhysician's question: ${question}` }],
      });
      setMessages(m => [...m, { role:'assistant', text: answer }]);
    } catch (err) {
      setMessages(m => [...m, { role:'assistant', text: 'Sorry, I could not reach the clinical agent right now.' }]);
    } finally { setBusy(false); }
  }

  return (
    <div style={{
      position:'fixed', right:20, bottom:20, width:400, height:560, zIndex:200,
      background:'#fff', border:'1px solid #1A1A1A', borderRadius:6,
      boxShadow:'0 18px 50px rgba(0,0,0,0.22)', display:'flex', flexDirection:'column', overflow:'hidden',
    }}>
      <div style={{padding:'12px 14px', background:'#1A1A1A', color:'#fff', display:'flex', alignItems:'center', gap:10}}>
        <div style={{width:26, height:26, borderRadius:'50%', background:'#D32027', display:'flex', alignItems:'center', justifyContent:'center', fontSize:13, fontWeight:800}}>✦</div>
        <div style={{flex:1, minWidth:0}}>
          <div style={{fontSize:12, fontWeight:700}}>Clinical Agent</div>
          <div style={{fontSize:10, color:'rgba(255,255,255,0.65)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis'}}>{node.label.join(' ')} · {patient?patient.alias:'no patient'}</div>
        </div>
        <button onClick={onClose} style={{background:'transparent', border:'none', color:'#fff', cursor:'pointer', fontSize:18, padding:'0 4px'}}>×</button>
      </div>
      <div ref={scrollRef} style={{flex:1, overflowY:'auto', padding:14, background:'#FAFAFA', display:'flex', flexDirection:'column', gap:10}}>
        {messages.map((m, i) => (
          <div key={i} style={{
            alignSelf: m.role==='user'?'flex-end':'flex-start',
            maxWidth:'85%',
            padding:'8px 12px', borderRadius:6,
            background: m.role==='user'?'#1A1A1A':'#fff',
            color: m.role==='user'?'#fff':'#1A1A1A',
            border: m.role==='user'?'none':'1px solid #E5E5E7',
            fontSize:12, lineHeight:1.55,
          }}>
            {m.role==='user' ? m.text : <Markdown text={m.text}/>}
          </div>
        ))}
        {busy && (
          <div style={{alignSelf:'flex-start', padding:'8px 12px', border:'1px solid #E5E5E7', background:'#fff', borderRadius:6, fontSize:12, color:'#AEAEB2'}}>thinking…</div>
        )}
      </div>
      {messages.length<=1 && (
        <div style={{padding:'8px 12px 4px', background:'#FAFAFA', borderTop:'1px solid #E5E5E7'}}>
          <div style={{fontSize:9, fontWeight:700, color:'#AEAEB2', textTransform:'uppercase', letterSpacing:'0.05em', marginBottom:5}}>Try</div>
          <div style={{display:'flex', flexWrap:'wrap', gap:5}}>
            {suggestions.map((s,i)=>(
              <button key={i} onClick={()=>send(s)} style={{padding:'4px 9px', background:'#fff', border:'1px solid #E5E5E7', borderRadius:12, fontSize:10, color:'#1A1A1A', cursor:'pointer'}}>{s}</button>
            ))}
          </div>
        </div>
      )}
      <form onSubmit={e=>{e.preventDefault(); send();}} style={{padding:10, borderTop:'1px solid #E5E5E7', background:'#fff', display:'flex', gap:6}}>
        <input
          value={input} onChange={e=>setInput(e.target.value)}
          placeholder="Ask about this step…"
          style={{flex:1, padding:'8px 10px', border:'1px solid #E5E5E7', borderRadius:4, fontSize:12, outline:'none'}}
        />
        <button type="submit" disabled={busy||!input.trim()} style={{padding:'8px 14px', background:'#D32027', color:'#fff', border:'none', borderRadius:4, fontSize:11, fontWeight:700, cursor:busy?'default':'pointer', opacity:busy||!input.trim()?0.5:1}}>Send</button>
      </form>
    </div>
  );
}

// ── Node Detail Panel ───────────────────────────────────────────────────────
function NodeDetail({ node, patient, onClose, onOpenChat }) {
  const ph = PHASES.find(p=>p.id===node.phase);
  const metaKeys = node.meta ? Object.keys(node.meta) : [];
  return (
    <div style={{
      position:'absolute', top:0, right:0, width:420, height:'100%', zIndex:50,
      background:'#fff', borderLeft:'1px solid #E5E5E7', boxShadow:'-8px 0 20px rgba(0,0,0,0.06)',
      display:'flex', flexDirection:'column', overflow:'hidden'
    }}>
      {/* Header */}
      <div style={{padding:'14px 18px 12px', borderBottom:'1px solid #E5E5E7', background:'#fff', flexShrink:0}}>
        <div style={{display:'flex', alignItems:'center', gap:8, marginBottom:6}}>
          <span style={{padding:'2px 8px', borderRadius:3, background:`${ph.color}14`, color:ph.color, fontSize:9, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.06em'}}>{ph.short}</span>
          {node.anchor && <span style={{padding:'2px 8px', borderRadius:3, background:'#D32027', color:'#fff', fontSize:9, fontWeight:700, letterSpacing:'0.05em'}}>★ DICE-CD ANCHOR</span>}
          {node.entry && <span style={{padding:'2px 8px', borderRadius:3, background:'#FEFCF8', color:'#7A6545', border:'1px solid #7A654530', fontSize:9, fontWeight:700, letterSpacing:'0.05em'}}>ENTRY</span>}
          {node.terminal && <span style={{padding:'2px 8px', borderRadius:3, background:'#FAFAFA', color:'#4A4A4A', border:'1px solid #E5E5E7', fontSize:9, fontWeight:700, letterSpacing:'0.05em'}}>TERMINAL</span>}
          {node.optional && <span style={{padding:'2px 8px', borderRadius:3, background:'#F2F3F5', color:'#4A4A4A', fontSize:9, fontWeight:700, letterSpacing:'0.05em'}}>OPTIONAL</span>}
          <button onClick={onClose} style={{marginLeft:'auto', background:'transparent', border:'none', color:'#4A4A4A', cursor:'pointer', fontSize:18}}>×</button>
        </div>
        <h2 style={{fontSize:15, fontWeight:700, color:'#1A1A1A', lineHeight:1.3}}>{node.label.join(' ')}</h2>
        <p style={{fontSize:11, color:'#4A4A4A', marginTop:5, lineHeight:1.5}}>{node.desc}</p>
      </div>

      {/* Scrollable body */}
      <div style={{flex:1, overflowY:'auto', padding:'14px 18px 80px'}}>
        {/* Cohort + metrics strip */}
        {node.cohort && (
          <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:8, marginBottom:14}}>
            <div style={{padding:'8px 10px', background:'#FAFAFA', border:'1px solid #E5E5E7', borderRadius:4}}>
              <div style={{fontSize:9, fontWeight:700, color:'#AEAEB2', textTransform:'uppercase', letterSpacing:'0.05em'}}>Cohort Reach</div>
              <div style={{fontSize:16, fontWeight:700, color:'#1A1A1A'}}>{node.cohort.n}<span style={{fontSize:11, fontWeight:400, color:'#4A4A4A'}}> / 312 · {node.cohort.pct}%</span></div>
            </div>
            <div style={{padding:'8px 10px', background:'#FAFAFA', border:'1px solid #E5E5E7', borderRadius:4}}>
              <div style={{fontSize:9, fontWeight:700, color:'#AEAEB2', textTransform:'uppercase', letterSpacing:'0.05em'}}>Median Dwell</div>
              <div style={{fontSize:14, fontWeight:700, color:'#1A1A1A', lineHeight:1.5}}>{node.cohort.dwellMedian}</div>
            </div>
          </div>
        )}

        {/* Guideline */}
        {node.guideline && (
          <div style={{marginBottom:14}}>
            <div style={{fontSize:9, fontWeight:700, color:'#AEAEB2', textTransform:'uppercase', letterSpacing:'0.06em', marginBottom:4}}>Primary Reference</div>
            <div style={{fontSize:11, color:'#1A1A1A', padding:'6px 10px', background:'#F4F7FC', border:'1px solid #2D5A8E30', borderRadius:4, fontWeight:500}}>{node.guideline}</div>
          </div>
        )}

        {/* Innovation banner */}
        {node.innovation && (
          <div style={{marginBottom:14, padding:'8px 10px', background:'#FFF5F5', border:'1px solid #D3202730', borderRadius:4, fontSize:11, color:'#D32027', fontWeight:600}}>
            {node.innovation}
          </div>
        )}

        {/* Meta sections */}
        {metaKeys.map(k => {
          const val = node.meta[k];
          const title = k.replace(/([A-Z])/g,' $1').replace(/^./,c=>c.toUpperCase()).trim();
          if (Array.isArray(val)) {
            return (
              <div key={k} style={{marginBottom:12}}>
                <div style={{fontSize:9, fontWeight:700, color:'#AEAEB2', textTransform:'uppercase', letterSpacing:'0.06em', marginBottom:5}}>{title}</div>
                <ul style={{listStyle:'none', padding:0, margin:0}}>
                  {val.map((v,i)=>(
                    <li key={i} style={{fontSize:11, color:'#1A1A1A', padding:'4px 0 4px 14px', position:'relative', lineHeight:1.5}}>
                      <span style={{position:'absolute', left:0, top:9, width:4, height:4, borderRadius:'50%', background:'#D32027'}}/>
                      {v}
                    </li>
                  ))}
                </ul>
              </div>
            );
          }
          return (
            <div key={k} style={{marginBottom:12}}>
              <div style={{fontSize:9, fontWeight:700, color:'#AEAEB2', textTransform:'uppercase', letterSpacing:'0.06em', marginBottom:4}}>{title}</div>
              <div style={{fontSize:11, color:'#1A1A1A', lineHeight:1.5}}>{val}</div>
            </div>
          );
        })}

        {/* Key metrics */}
        {node.keyMetrics && (
          <div style={{marginBottom:14}}>
            <div style={{fontSize:9, fontWeight:700, color:'#AEAEB2', textTransform:'uppercase', letterSpacing:'0.06em', marginBottom:5}}>Key Metrics (cohort)</div>
            <table style={{width:'100%', fontSize:11, borderCollapse:'collapse'}}>
              <tbody>
                {node.keyMetrics.map((m,i)=>(
                  <tr key={i} style={{borderBottom:'1px solid #F0F0F0'}}>
                    <td style={{padding:'6px 0', color:'#4A4A4A'}}>{m.k}</td>
                    <td style={{padding:'6px 0', color:'#1A1A1A', fontWeight:600, textAlign:'right'}}>{m.v}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}

        {/* Data sources */}
        {node.dataSources && node.dataSources.length>0 && (
          <div style={{marginBottom:14}}>
            <div style={{fontSize:9, fontWeight:700, color:'#AEAEB2', textTransform:'uppercase', letterSpacing:'0.06em', marginBottom:5}}>Data Sources (via MeDIC)</div>
            <div style={{display:'flex', flexWrap:'wrap', gap:5}}>
              {node.dataSources.map(d=>{
                const isHub = d==='medic';
                return (
                  <span key={d} style={{padding:'3px 8px', fontSize:10, fontWeight:600, borderRadius:3,
                    background: isHub?'#1A1A1A':'#F7F7F8', color: isHub?'#fff':'#1A1A1A',
                    border: isHub?'1px solid #D32027':'1px solid #E5E5E7'}}>
                    {DS_LABELS[d] || d}
                  </span>
                );
              })}
            </div>
          </div>
        )}

        {/* Patient context (when selected) */}
        {patient && (
          <div style={{marginTop:16, padding:'10px 12px', background:'#FFF5F5', border:'1px solid #D3202720', borderRadius:4}}>
            <div style={{fontSize:9, fontWeight:700, color:'#D32027', textTransform:'uppercase', letterSpacing:'0.06em', marginBottom:5}}>For {patient.alias}</div>
            <div style={{fontSize:11, color:'#1A1A1A', lineHeight:1.5}}>
              {patient.sex==='F'?'♀':'♂'} age {patient.age} · eGFR {patient.egfr||'—'} · Mayo {patient.mayoClass||'—'} · {patient.riskTier||'—'} risk
            </div>
          </div>
        )}
      </div>

      {/* Sticky action bar */}
      <div style={{padding:'10px 18px', borderTop:'1px solid #E5E5E7', background:'#fff', display:'flex', gap:8, flexShrink:0}}>
        {node.anchor ? (
          <button onClick={onOpenChat} style={{
            flex:1, padding:'9px 12px', background:'#1A1A1A', color:'#fff', border:'none',
            borderRadius:4, fontSize:11, fontWeight:700, cursor:'pointer',
            display:'flex', alignItems:'center', justifyContent:'center', gap:6,
          }}>
            <span style={{width:14, height:14, borderRadius:'50%', background:'#D32027', display:'inline-flex', alignItems:'center', justifyContent:'center', fontSize:9, fontWeight:800}}>✦</span>
            Chat with Agent
          </button>
        ) : (
          <button disabled title="Agent chat is only available on DICE-CD anchor steps (marked with ★)" style={{
            flex:1, padding:'9px 12px', background:'#F2F3F5', color:'#AEAEB2', border:'1px solid #E5E5E7',
            borderRadius:4, fontSize:11, fontWeight:700, cursor:'not-allowed',
            display:'flex', alignItems:'center', justifyContent:'center', gap:6,
          }}>
            <span style={{fontSize:10}}>★</span>
            Agent on anchor steps
          </button>
        )}
        <button disabled title="Coming soon" style={{
          flex:1, padding:'9px 12px', background:'#F2F3F5', color:'#AEAEB2', border:'1px solid #E5E5E7',
          borderRadius:4, fontSize:11, fontWeight:700, cursor:'not-allowed',
          display:'flex', alignItems:'center', justifyContent:'center', gap:6,
        }}>
          Deep Dive
          <span style={{fontSize:8, fontWeight:700, padding:'1px 5px', background:'#E5E5E7', borderRadius:2, color:'#4A4A4A'}}>SOON</span>
        </button>
      </div>
    </div>
  );
}

// ── Legend ──────────────────────────────────────────────────────────────────
function Legend({ view }) {
  return (
    <div style={{display:'flex', gap:14, padding:'10px 14px', background:'#FAFAFA', border:'1px solid #E5E5E7', borderRadius:5, fontSize:11, color:'#4A4A4A', flexWrap:'wrap', alignItems:'center'}}>
      {view==='patient' ? (
        <>
          <span style={{display:'flex',alignItems:'center',gap:5}}><span style={{width:20,height:2,background:'#D32027',display:'inline-block'}}/> Traversed</span>
          <span style={{display:'flex',alignItems:'center',gap:5}}><span style={{width:20,height:0,borderTop:'2px dashed #D32027',display:'inline-block'}}/> Forecast</span>
          <span style={{display:'flex',alignItems:'center',gap:5}}><span style={{width:20,height:1,background:'#E5E5E7',display:'inline-block'}}/> Not traversed</span>
          <span style={{display:'flex',alignItems:'center',gap:5}}><span style={{width:10,height:10,borderRadius:2,background:'#D32027',display:'inline-block'}}/> DICE-CD anchor</span>
        </>
      ) : view==='peers' ? (
        <>
          <span>Edge labels show peer-group %</span>
          <span style={{display:'flex',alignItems:'center',gap:5}}><span style={{padding:'1px 5px',background:'#D1FAE5',color:'#047857',borderRadius:2,fontSize:9,fontWeight:700}}>+Δpp</span> higher in peers</span>
          <span style={{display:'flex',alignItems:'center',gap:5}}><span style={{padding:'1px 5px',background:'#FEE2E2',color:'#B91C1C',borderRadius:2,fontSize:9,fontWeight:700}}>−Δpp</span> lower in peers</span>
        </>
      ) : (
        <>
          <span>Edge labels = transition rate over n=312</span>
          <span style={{display:'flex',alignItems:'center',gap:5}}><span style={{width:20,height:3,background:'rgba(211,32,39,0.25)',display:'inline-block'}}/> Dominant cohort path</span>
          <span style={{display:'flex',alignItems:'center',gap:5}}><span style={{width:20,height:0,borderTop:'1.5px dashed #C77700',display:'inline-block'}}/> Loop-back (plan revision)</span>
          <span style={{display:'flex',alignItems:'center',gap:5}}><span style={{width:10,height:10,borderRadius:2,background:'#D32027',display:'inline-block'}}/> DICE-CD anchor</span>
        </>
      )}
      <span style={{marginLeft:'auto', fontSize:10}}>Source: DIZ-UKK cohort export · n=312 · updated 2026-04-15</span>
    </div>
  );
}

// ── Edge Tooltip ────────────────────────────────────────────────────────────
function EdgeTooltip({ data, mousePos, containerRef }) {
  if (!data || !mousePos || !containerRef.current) return null;
  const cw = containerRef.current.offsetWidth;
  const left = Math.min(mousePos.x + 14, cw - 280);
  return (
    <div style={{
      position:'absolute', left, top: Math.max(mousePos.y - 10, 8), zIndex:60, pointerEvents:'none',
      background:'#fff', border:'1px solid #E5E5E7', borderRadius:5,
      boxShadow:'0 4px 18px rgba(0,0,0,0.12)', padding:'10px 14px', maxWidth:274
    }}>
      <div style={{fontSize:12, fontWeight:700, color:'#1A1A1A', marginBottom:4}}>{data.title}</div>
      {data.big && <div style={{fontSize:18, fontWeight:800, color:'#D32027', lineHeight:1, marginBottom:4}}>{data.big} <span style={{fontSize:11, fontWeight:400, color:'#4A4A4A'}}>{data.bigSub}</span></div>}
      {data.annotation && <div style={{fontSize:11, fontWeight:600, color:'#2E7D5B', marginBottom:4, padding:'3px 6px', background:'#F0FDF4', borderRadius:3}}>{data.annotation}</div>}
      {data.desc && <div style={{fontSize:11, color:'#4A4A4A', lineHeight:1.5}}>{data.desc}</div>}
    </div>
  );
}

// ── Header ──────────────────────────────────────────────────────────────────
function JourneyHeader({ view, setView, patient }) {
  const VIEWS = [
    { id:'cohort',  label:'Overall Cohort',   sub:'aggregate transition rates' },
    { id:'patient', label:'Selected Patient', sub: patient ? patient.alias : 'select a patient' },
    { id:'peers',   label:'Close References', sub: patient ? `peer group · ${patient.sex==='F'?'F':'M'}, ±15 yr, ${patient.riskTier}` : 'select a patient' },
  ];
  return (
    <div style={{padding:'14px 24px 12px', borderBottom:'1px solid #E5E5E7', background:'#fff', flexShrink:0}}>
      <div style={{marginBottom:10}}>
        <h1 style={{fontSize:17, fontWeight:700, color:'#1A1A1A', letterSpacing:'-0.01em', marginBottom:2}}>
          ADPKD Patient Journey · Reference Model (v0.1)
        </h1>
        <p style={{fontSize:11, color:'#AEAEB2'}}>Click a step to inspect details, ask the agent, or open the deep dive</p>
      </div>
      <div style={{display:'flex', gap:6}}>
        {VIEWS.map(v=>{
          const disabled=v.id!=='cohort'&&!patient;
          return (
            <button key={v.id} onClick={()=>!disabled&&setView(v.id)} style={{
              padding:'7px 14px', borderRadius:4, fontSize:12, fontWeight:600, textAlign:'left',
              border: view===v.id?'2px solid #1A1A1A':'1px solid #E5E5E7',
              background: view===v.id?'#1A1A1A':'#fff',
              color: view===v.id?'#fff':disabled?'#D1D1D6':'#4A4A4A',
              cursor: disabled?'default':'pointer', opacity: disabled?0.5:1
            }}>
              {v.label}
              <div style={{fontSize:10,fontWeight:400,marginTop:1,opacity:0.65}}>{v.sub}</div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ── Main ────────────────────────────────────────────────────────────────────
function PipelineView({ patients, selectedId, onSelect, onNavigate }) {
  const [view, setView] = usePV('cohort');
  const [hoveredNode, setHoveredNode] = usePV(null);
  const [hoveredEdge, setHoveredEdge] = usePV(null);
  const [selectedNode, setSelectedNode] = usePV(null);
  const [chatNode, setChatNode] = usePV(null);
  const [tooltipData, setTooltipData] = usePV(null);
  const [mousePos, setMousePos] = usePV(null);
  const containerRef = useRefPV(null);

  const patient = patients.find(p=>p.id===selectedId) || null;
  const { traversed, forecast } = getPatientEdges(patient);
  const dominantSet = new Set();
  for (let i=0;i<DOMINANT_PATH.length-1;i++) dominantSet.add(`${DOMINANT_PATH[i]}-${DOMINANT_PATH[i+1]}`);

  function handleMouseMove(e) {
    if (!containerRef.current) return;
    const r = containerRef.current.getBoundingClientRect();
    setMousePos({ x: e.clientX - r.left, y: e.clientY - r.top });
  }

  function hoverEdge(e) {
    setHoveredEdge(e ? edgeKey(e) : null);
    if (!e) { setTooltipData(null); return; }
    const f = nodeById(e.f), t = nodeById(e.t);
    const delta = view==='peers' ? PEER_DELTAS[edgeKey(e)] : undefined;
    setTooltipData({
      title: `${f.label.join(' ')} → ${t.label.join(' ')}`,
      big: view==='patient' ? undefined : `${e.p}%`,
      bigSub: view==='patient' ? undefined : 'of cohort',
      desc: e.rationale || (e.loopback ? 'Patient returns to Treatment Planning for regimen revision.' : null),
      annotation: delta !== undefined ? `Peer group: ${e.p + delta}% (${delta>0?'+':''}${delta}pp vs overall)` : null,
    });
  }

  return (
    <div style={{height:'100%', display:'flex', flexDirection:'column', overflow:'hidden'}}>
      <JourneyHeader view={view} setView={setView} patient={patient}/>

      {view==='patient' && (
        <div style={{padding:'8px 24px', background:'#FFFBEB', borderBottom:'1px solid #E5E5E7', fontSize:11, color:'#C77700'}}>
          ⚠ Forecast edges are model-based estimates, not clinical predictions.
        </div>
      )}

      <div ref={containerRef} style={{flex:1, overflow:'auto', position:'relative', padding:'16px 24px 20px'}}
        onMouseMove={handleMouseMove}>

        <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:12, gap:12}}>
          <p style={{fontSize:11, color:'#4A4A4A'}}>
            {view==='cohort' && 'Hover edges to inspect rates · Click nodes for details, agent chat, and deep dive'}
            {view==='patient' && `Pathway for ${patient?.alias} · Red solid = traversed · Red dashed = forecast`}
            {view==='peers' && `Peer-group transition rates shown on edges · Δpp vs overall in tooltip`}
          </p>
          {patient && (
            <button onClick={()=>onNavigate('dashboard')} style={{
              padding:'6px 12px', background:'#D32027', color:'#fff', border:'none',
              borderRadius:4, fontSize:11, fontWeight:700, cursor:'pointer', flexShrink:0,
            }}>Patient Dashboard – {patient.alias} →</button>
          )}
        </div>

        <div style={{background:'#fff', border:'1px solid #E5E5E7', borderRadius:5, overflow:'auto', marginBottom:12}}>
          <svg width={CW} height={CH} style={{display:'block'}}>
            <defs>
              <marker id="ah-grey" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#AEAEB2"/></marker>
              <marker id="ah-red" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#D32027"/></marker>
              <marker id="ah-amber" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#C77700"/></marker>
              <marker id="ah-faint" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#E5E5E7"/></marker>
            </defs>

            {PHASES.map(ph => (
              <g key={ph.id}>
                <rect x={LANE_X-10} y={ph.y} width={LANE_W+20} height={ph.h} rx={4} fill={ph.bg} stroke={`${ph.color}30`} strokeWidth={1}/>
                <text x={LANE_X-2} y={ph.y+16} fontSize={9} fontWeight={700} fill={ph.color} fontFamily="Inter,sans-serif" letterSpacing={0.5}>{ph.short.toUpperCase()}</text>
              </g>
            ))}

            {EDGES.filter(e=>!e.hidden).map(e => {
              const d = routeEdge(e); if (!d) return null;
              const k = edgeKey(e);
              const isDominant = dominantSet.has(k);
              const isTraversed = traversed.includes(k);
              const isForecast = forecast.includes(k);
              const isHoveredEdge = hoveredEdge === k;
              const isAdjacentNode = (hoveredNode && (e.f===hoveredNode || e.t===hoveredNode)) || (selectedNode && (e.f===selectedNode.id || e.t===selectedNode.id));

              let stroke='#AEAEB2', sw=1, dash=undefined, op=0.7, marker='ah-grey';
              if (e.loopback) { stroke='#C77700'; dash='4 3'; marker='ah-amber'; }
              if (view==='patient') {
                if (isTraversed) { stroke='#D32027'; sw=2; op=1; marker='ah-red'; }
                else if (isForecast) { stroke='#D32027'; sw=1.5; dash='5 3'; op=0.9; marker='ah-red'; }
                else { stroke='#E5E5E7'; sw=1; op=0.4; marker='ah-faint'; }
              } else {
                if (isDominant) { stroke='rgba(211,32,39,0.45)'; sw=2.5; op=0.9; marker='ah-red'; }
                if (e.anchor) { stroke='#D32027'; sw=2; op=0.85; marker='ah-red'; }
              }
              if (isHoveredEdge || isAdjacentNode) { stroke='#D32027'; sw=2.5; op=1; marker='ah-red'; dash=e.loopback?'4 3':undefined; }

              const p = view==='peers' && PEER_DELTAS[k] !== undefined ? e.p + PEER_DELTAS[k] : e.p;
              const delta = view==='peers' ? PEER_DELTAS[k] : undefined;
              const mid = midpointOfPath(e);
              const showLabel = e.branch || e.loopback || e.subtle;

              return (
                <g key={k} style={{cursor:'pointer'}} onMouseEnter={()=>hoverEdge(e)} onMouseLeave={()=>hoverEdge(null)}>
                  <path d={d} fill="none" stroke={stroke} strokeWidth={sw} strokeDasharray={dash} opacity={op} markerEnd={`url(#${marker})`}/>
                  <path d={d} fill="none" stroke="transparent" strokeWidth={14}/>
                  {showLabel && (
                    <g>
                      <rect x={mid.x - 20} y={mid.y - 8} width={40} height={15} rx={3} fill="#fff" stroke={isHoveredEdge?'#D32027':'#E5E5E7'} strokeWidth={1}/>
                      <text x={mid.x} y={mid.y + 3} textAnchor="middle" fontSize={9} fontWeight={700} fill={isHoveredEdge?'#D32027':'#1A1A1A'} fontFamily="Inter,sans-serif">{p}%</text>
                      {delta !== undefined && (
                        <g>
                          <rect x={mid.x + 21} y={mid.y - 8} width={26} height={15} rx={3} fill={delta>0?'#D1FAE5':'#FEE2E2'} stroke="none"/>
                          <text x={mid.x + 34} y={mid.y + 3} textAnchor="middle" fontSize={9} fontWeight={700} fill={delta>0?'#047857':'#B91C1C'} fontFamily="Inter,sans-serif">{delta>0?'+':''}{delta}</text>
                        </g>
                      )}
                    </g>
                  )}
                </g>
              );
            })}

            {NODES.map(n => {
              const isHov = hoveredNode === n.id;
              const isSelected = selectedNode && selectedNode.id === n.id;
              const isAdjacent = hoveredEdge && (hoveredEdge.startsWith(`${n.id}-`) || hoveredEdge.endsWith(`-${n.id}`));
              const isOnTraversed = view==='patient' && (traversed.some(k=>k.startsWith(`${n.id}-`)||k.endsWith(`-${n.id}`)));

              let fill='#fff', stroke='#D1D1D6', sw=1, textColor='#1A1A1A';
              if (n.hub) { fill='#1A1A1A'; stroke='#1A1A1A'; textColor='#fff'; }
              if (n.entry) { stroke='#7A6545'; fill='#FEFCF8'; }
              if (n.terminal) { stroke='#4A4A4A'; fill='#FAFAFA'; }
              if (view==='patient' && isOnTraversed && !n.hub) { stroke='#D32027'; sw=2; }
              if (isHov || isAdjacent) { stroke='#D32027'; sw=2; }
              if (isSelected) { stroke='#D32027'; sw=3; }

              return (
                <g key={n.id} style={{cursor:'pointer'}}
                  onMouseEnter={()=>setHoveredNode(n.id)} onMouseLeave={()=>setHoveredNode(null)}
                  onClick={()=>setSelectedNode(n)}>
                  <rect x={n.x} y={n.y} width={n.w} height={n.h} rx={5} fill={fill} stroke={stroke} strokeWidth={sw} strokeDasharray={n.optional?'4 3':undefined}/>
                  {n.label.map((ln, i) => (
                    <text key={i} x={n.x + n.w/2} y={n.y + n.h/2 + (n.label.length===1?3:-4) + i*13}
                      textAnchor="middle" fontSize={n.hub?11:10.5} fontWeight={n.hub?700:600}
                      fill={textColor} fontFamily="Inter,sans-serif">{ln}</text>
                  ))}
                  {n.anchor && (
                    <g>
                      <rect x={n.x + n.w - 14} y={n.y + 4} width={10} height={10} rx={2} fill="#D32027"/>
                      <text x={n.x + n.w - 9} y={n.y + 12} textAnchor="middle" fontSize={7} fontWeight={700} fill="#fff" fontFamily="Inter,sans-serif">★</text>
                    </g>
                  )}
                  {n.entry && (
                    <text x={n.x + 6} y={n.y + 12} fontSize={7} fontWeight={700} fill="#7A6545" fontFamily="Inter,sans-serif" letterSpacing={0.5}>ENTRY</text>
                  )}
                  {n.terminal && (
                    <text x={n.x + n.w - 6} y={n.y + n.h - 5} textAnchor="end" fontSize={7} fontWeight={700} fill="#4A4A4A" fontFamily="Inter,sans-serif" letterSpacing={0.5}>TERMINAL</text>
                  )}
                </g>
              );
            })}

            <text x={CW - 50} y={860} textAnchor="end" fontSize={9} fontWeight={700} fill="#C77700" fontFamily="Inter,sans-serif" letterSpacing={0.4}>
              LOOP-BACK: PLAN REVISION (35%)
            </text>
          </svg>
        </div>

        <Legend view={view}/>

        <EdgeTooltip data={tooltipData} mousePos={mousePos} containerRef={containerRef}/>

        {selectedNode && (
          <NodeDetail node={selectedNode} patient={patient}
            onClose={()=>setSelectedNode(null)}
            onOpenChat={()=>setChatNode(selectedNode)}/>
        )}
      </div>

      {chatNode && (
        <AgentChat node={chatNode} patient={patient} onClose={()=>setChatNode(null)}/>
      )}
    </div>
  );
}

Object.assign(window, { PipelineView });
