// silo-engine.jsx — the synthetic resident's reasoning. No JSX, no rendering.
//
// Every finding is DERIVED from chart-data.jsx and note-data.jsx at runtime.
// Nothing here is a hardcoded card. If the chart changes, the findings change;
// if the data doesn't support a claim, no card is emitted.
//
// This matters more than it sounds. While designing this panel we nearly shipped
// a card saying magnesium was low and unrepleted — it dipped to 1.6 on 04/29 but
// is 1.9 and normal today. The rule below (a value only "normalises" if the
// PREVIOUS resulted value was flagged and the latest is not) is what keeps that
// card from ever being generated. See the node assertions in the verification run.
//
// Exports on window: deriveFindings, noteDigest, SILO_CONTEXT

const _num = v => { const m = String(v).match(/-?[\d.]+/); return m ? parseFloat(m[0]) : null; };
const _abn = v => /[↑↓]/.test(String(v));
const _resulted = v => v !== '—' && v != null;
// Display value: the number exactly as the lab filed it, minus the flag arrow.
// _num() is for comparisons only — it turns "3.0" into 3 and drops the decimal.
const _disp = v => String(v).replace(/[↑↓!]/g, '').trim();

function _lab(name) { return window.LABS.find(l => l.name === name); }

// index of the most recent resulted value
function _lastIdx(row) {
  for (let i = row.vals.length - 1; i >= 0; i--) if (_resulted(row.vals[i])) return i;
  return null;
}
function _prevIdx(row, before) {
  for (let i = before - 1; i >= 0; i--) if (_resulted(row.vals[i])) return i;
  return null;
}
function _latest(name) {
  const r = _lab(name), i = _lastIdx(r);
  return i == null ? null : { raw: r.vals[i], d: _disp(r.vals[i]), n: _num(r.vals[i]), date: window.LAB_DATES[i], abn: _abn(r.vals[i]), ref: r.ref, isLatestCol: i === r.vals.length - 1 };
}
function _firstResulted(name) {
  const r = _lab(name);
  for (let i = 0; i < r.vals.length; i++) if (_resulted(r.vals[i]))
    return { raw: r.vals[i], d: _disp(r.vals[i]), n: _num(r.vals[i]), date: window.LAB_DATES[i] };
  return null;
}
function _extreme(name, dir) {
  const r = _lab(name); let best = null;
  r.vals.forEach((v, i) => {
    if (!_resulted(v)) return;
    const n = _num(v);
    if (best == null || (dir === 'max' ? n > best.n : n < best.n)) best = { n, raw: v, d: _disp(v), date: window.LAB_DATES[i] };
  });
  return best;
}
const _med = re => window.MEDS.filter(m => re.test(m.name));
const _hasProblem = re => window.PROBLEMS.some(p => re.test(p.name));
const _consult = re => window.CONSULTS.find(c => re.test(c.service));

function deriveFindings() {
  const F = [];
  const PT = window.PATIENT;

  // ── 1. A hold whose stated condition has resolved ────────────────────────
  // Only fires if a drug is actually HELD *and* the lab it was held for is
  // back in range. Both halves are read from the chart, not assumed.
  const held = window.MEDS.filter(m => m.status === 'HELD');
  const k = _latest('Potassium'), kPeak = _extreme('Potassium', 'max');
  if (held.length && k && !k.abn && kPeak && _abn(kPeak.raw)) {
    const drug = held[0];
    F.push({
      id: 'acei-hold', group: 'decision', kind: 'Contradiction',
      title: `${drug.name} is held; the condition it was held for has resolved`,
      body: `The plan of 04/29 conditioned the ${drug.name.toLowerCase()} hold on potassium stabilising. Potassium ${kPeak.d} (${kPeak.date}) → ${k.d} (${k.date}), inside the ${k.ref} range. ${drug.name} still shows status HELD in the MAR.`,
      sources: [
        { pane: 'notes', label: 'Progress note 04/29' },
        { pane: 'results', label: `Potassium ${k.d}` },
        { pane: 'mar', label: `${drug.name} — HELD` },
      ],
      actions: ['insert', 'order', 'source'],
      insertText: `${drug.name} (held since admission per 04/29 plan pending potassium stabilisation): potassium now ${k.d} mEq/L (${k.date}), within range. Restarting ${drug.name.toLowerCase()} ${drug.sig} today; recheck basic metabolic panel in 48 hours.`,
      orderDrug: 'lisinopril',
    });
  }

  // ── 2. A dose sitting against the renal recommendation ───────────────────
  const vanc = _med(/^Vancomycin$/)[0];
  const trough = _latest('Vanc trough'), egfr = _latest('eGFR');
  const pharm = _consult(/Pharmacy/);
  if (vanc && trough && egfr) {
    const [lo, hi] = (trough.ref.match(/[\d.]+/g) || []).map(parseFloat);
    const nearTop = hi && trough.n >= lo + (hi - lo) * 0.6;
    if (nearTop) {
      F.push({
        id: 'vanc-dose', group: 'decision', kind: 'Contradiction',
        title: 'Vancomycin dose sits above the renal-dosing recommendation',
        body: `${vanc.sig} is active. ${pharm ? pharm.service + ' (' + pharm.md + ', last note ' + pharm.lastNote + ')' : 'Pharmacy'} recommends 1,000 mg q24h at this clearance. Trough ${trough.d} mcg/mL on ${trough.date} — upper end of the ${trough.ref} target — with eGFR ${egfr.d}.`,
        sources: [
          { pane: 'mar', label: `${vanc.name} ${vanc.sig.split('—')[0].trim()}` },
          { pane: 'results', label: `Trough ${trough.d}` },
          { pane: 'snapshot', label: 'Pharmacy consult' },
        ],
        actions: ['insert', 'source'],
        insertText: `Vancomycin trough ${trough.d} mcg/mL (${trough.date}), upper end of the ${trough.ref} target, eGFR ${egfr.d}. Pharmacy recommends 1,000 mg q24h at this clearance; current order is ${vanc.sig.split('—')[0].trim()}. Will reconcile dosing with pharmacy today.`,
      });
    }
  }

  // ── 3. A problem on the list that nothing is being done about ────────────
  const hgb0 = _firstResulted('Hemoglobin'), hgb1 = _latest('Hemoglobin');
  const anaemiaRx = _med(/iron|ferric|ferrous|epoetin|darbepoetin|Aranesp|Retacrit/i);
  if (hgb0 && hgb1 && hgb1.n < hgb0.n && _hasProblem(/[Aa]nemia|[Aa]naemia/) && anaemiaRx.length === 0) {
    F.push({
      id: 'anaemia', group: 'watch', kind: 'Unaddressed',
      title: `Haemoglobin down ${(hgb0.n - hgb1.n).toFixed(1)} g/dL with nothing ordered`,
      body: `${hgb0.d} → ${hgb1.d} g/dL between ${hgb0.date} and ${hgb1.date}. Anaemia of chronic kidney disease is on the active problem list. No iron studies, erythropoiesis-stimulating agent, or transfusion threshold appears in the ${window.MEDS.length} active orders.`,
      sources: [
        { pane: 'results', label: `Haemoglobin ${hgb1.d}` },
        { pane: 'problems', label: 'Anaemia of CKD' },
        { pane: 'mar', label: `${window.MEDS.length} active orders` },
      ],
      actions: ['insert', 'source'],
      insertText: `Haemoglobin ${hgb0.d} → ${hgb1.d} g/dL over the admission in the setting of anaemia of chronic kidney disease. Will send iron studies and reticulocyte count today.`,
    });
  }

  // ── 4. A falling value with no service looking at it ─────────────────────
  const alb0 = _firstResulted('Albumin'), alb1 = _latest('Albumin');
  if (alb0 && alb1 && alb1.n < alb0.n && !_consult(/Nutrition|Dietet/i)) {
    F.push({
      id: 'albumin', group: 'watch', kind: 'Unaddressed',
      title: 'Albumin falling; no nutrition input recorded',
      body: `${alb0.d} → ${alb1.d} g/dL (${alb0.date} to ${alb1.date}), below the ${alb1.ref} range throughout. ${window.CONSULTS.length} services are following this patient; none is nutrition.`,
      sources: [
        { pane: 'results', label: `Albumin ${alb1.d}` },
        { pane: 'snapshot', label: 'Consulting services' },
      ],
      actions: ['insert', 'source'],
      insertText: `Albumin ${alb0.d} → ${alb1.d} g/dL over the admission. Will request nutrition consult for assessment of protein-energy status.`,
    });
  }

  // ── 5. A consult that has stalled ────────────────────────────────────────
  const stalled = window.CONSULTS.filter(c => /pending/i.test(c.status));
  stalled.forEach(c => F.push({
    id: 'consult-' + c.service.toLowerCase().replace(/\W+/g, '-'), group: 'watch', kind: 'Open thread',
    title: `${c.service} sign-off still outstanding`,
    body: `Status "${c.status}" since the last note on ${c.lastNote}. ${c.md} has filed nothing since.`,
    sources: [{ pane: 'snapshot', label: c.service }, { pane: 'notes', label: 'Notes filed' }],
    actions: ['source'],
  }));

  // ── 6. A care gap stated on the header and never closed ──────────────────
  if (/not on file/i.test(PT.advDirectives)) {
    F.push({
      id: 'adv-directives', group: 'watch', kind: 'Care gap',
      title: 'Advance directives not on file, day ' + PT.admitDay,
      body: `${PT.age}-year-old admitted ${PT.admitDay} days ago with ${PT.summary}, code status ${PT.code}. Advance directives remain "${PT.advDirectives}".`,
      sources: [{ pane: 'snapshot', label: 'Patient header' }],
      actions: ['source'],
    });
  }

  // ── 7. The trajectory, assembled across panes ────────────────────────────
  // In the baseline this answer is spread over Results, MAR, I/O and Notes.
  const cr = _latest('Creatinine'), crPeak = _extreme('Creatinine', 'max');
  const fena = _latest('FENa');
  const io = window.IO_DATA, ioLast = io[io.length - 1], ioPrev = io[io.length - 2];
  const diuretic = _med(/Furosemide|Bumetanide|Torsemide/i)[0];
  const wPeak = io.reduce((a, b) => (_num(b.weight) > _num(a.weight) ? b : a), io[0]);
  if (cr && crPeak && diuretic) {
    F.push({
      id: 'trajectory', group: 'decision', kind: 'Trajectory',
      title: 'Creatinine has turned, and the reason is documented in four places',
      body: `Creatinine peaked at ${crPeak.d} (${crPeak.date}) and is ${cr.d} today. ${diuretic.name} ${diuretic.sig} started ${diuretic.start}. Fluid balance ${ioPrev.net} then ${ioLast.net} mL; weight ${wPeak.weight} → ${ioLast.weight}.${fena ? ` FENa ${fena.d}%, consistent with a pre-renal picture.` : ''}`,
      sources: [
        { pane: 'results', label: `Creatinine ${cr.d}` },
        { pane: 'mar', label: `${diuretic.name} ${diuretic.start}` },
        { pane: 'io', label: `Net ${ioLast.net} mL` },
        { pane: 'notes', label: 'Prior A/P' },
      ],
      actions: ['insert', 'source'],
      insertText: `AKI on CKD: creatinine peaked ${crPeak.d} (${crPeak.date}), now ${cr.d}. ${diuretic.name} since ${diuretic.start} with net ${ioPrev.net} and ${ioLast.net} mL and weight ${wPeak.weight} → ${ioLast.weight}. ${fena ? `FENa ${fena.d}% supports a pre-renal aetiology. ` : ''}Trajectory is improving.`,
    });
  }

  // ── 8. What changed since the last look ──────────────────────────────────
  const normalised = [], fresh = [];
  const lastCol = window.LAB_DATES.length - 1;
  window.LABS.forEach(r => {
    const li = _lastIdx(r); if (li == null) return;
    // Only things filed in TODAY's column count as news. Without this, the
    // vancomycin peak (last resulted 04/28) was announced as newly resulted.
    if (li !== lastCol) return;
    const pi = _prevIdx(r, li);
    // "Newly resulted" means the immediately preceding column was blank.
    const isFresh = li > 0 && !_resulted(r.vals[li - 1]);
    // "Back in range" requires the previous RESULTED value to have been flagged.
    // This is the guard that keeps magnesium (1.6 low on 04/29, but 1.8 normal
    // before today's 1.9) out of the list.
    const isNormalised = pi != null && _abn(r.vals[pi]) && !_abn(r.vals[li]);
    // A first result is news as a result, not as a return to normal — otherwise
    // the vancomycin trough was announced twice in the same sentence.
    if (isFresh) fresh.push(`${r.name} ${_disp(r.vals[li])}`);
    else if (isNormalised) normalised.push(`${r.name} ${_disp(r.vals[li])}`);
  });
  if (normalised.length || fresh.length) {
    F.push({
      id: 'updates', group: 'update', kind: 'Since you last looked',
      title: `${normalised.length + fresh.length} results changed the picture this morning`,
      body: [
        normalised.length ? `Back in range: ${normalised.join(', ')}.` : '',
        fresh.length ? `Newly resulted: ${fresh.join(', ')}.` : '',
      ].filter(Boolean).join(' '),
      sources: [{ pane: 'results', label: `Filed ${window.LAB_DATES[window.LAB_DATES.length - 1]}` }],
      actions: ['source'],
    });
  }

  return F;
}

// ── Note digest ────────────────────────────────────────────────────────────
// "Yesterday in three lines" is the opening of each of yesterday's A/P threads.
// "New today" is the text actually tagged as original. Both come from the note.
function noteDigest(day) {
  // Split only where a period is followed by whitespace and a capital. Splitting
  // on bare periods truncated "Potassium 5.8 mEq/L" to "Potassium 5." in the digest.
  const twoSentences = t => t.split(/(?<=\.)\s+(?=[A-Z])/).slice(0, 2).join(' ').trim();
  const prior = window.buildNote(day - 1)
    .filter(s => s.section === 'Assessment and Plan')
    .flatMap(s => s.blocks).map(b => twoSentences(b.text));
  const today = window.buildNote(day)
    .flatMap(s => s.blocks).filter(b => b.origin === 'new').map(b => b.text);
  const stats = window.noteStats(window.buildNote(day));
  return {
    prior, today,
    newWords: stats.newWords,
    total: stats.total,
    carried: stats.total - stats.newWords,
  };
}

const SILO_CONTEXT = () => ({
  results: window.LABS.length,
  days: window.LAB_DATES.length,
  notes: window.PRIOR_NOTES.length,
  consults: window.CONSULTS.length,
  orders: window.MEDS.length,
  since: '04/25',
});

Object.assign(window, { deriveFindings, noteDigest, SILO_CONTEXT });
