// ============================================================
// calc-math.js — Pure, side-effect-free dose / score math.
// Dual-exported: browser (window._PED_MATH) + Node (module.exports).
// Keep this file free of DOM references so it can be unit-tested
// under Node with `node --test test/`.
// ============================================================
(function() {
  function roundTo(v, places) { var m = Math.pow(10, places); return Math.round(v * m) / m; }

  // ── Age → Weight ────────────────────────────────────────────
  // APLS (Luscombe & Owens 2007 / APLS-UK 2016) and Best Guess
  // (Tinning & Acworth 2007). For ages ≥13 yr we use Best Guess
  // as primary; otherwise APLS.
  function estimateWeightFromAgeMonths(months) {
    if (months == null || isNaN(months) || months < 0) return null;
    var years = months / 12;
    var apls;
    if (months < 12) apls = 0.5 * months + 4;
    else if (years <= 5) apls = 2 * years + 8;
    else apls = 3 * years + 7;
    var bg;
    if (months < 12) bg = (months + 9) / 2;
    else if (years <= 5) bg = 2 * (years + 5);
    else bg = 4 * years;
    var primary = years < 13 ? apls : bg;
    var bandLabel;
    if (years < 13) {
      if (months < 12) bandLabel = 'APLS 0-12 mo';
      else if (years <= 5) bandLabel = 'APLS 1-5 yr';
      else bandLabel = 'APLS 6-12 yr';
    } else {
      bandLabel = 'Best Guess 5-14 yr';
    }
    return {
      weight: Math.max(0.3, roundTo(primary, 1)),
      formulaLabel: bandLabel,
      all: { apls: roundTo(apls, 1), bestGuess: roundTo(bg, 1) }
    };
  }

  // ── Parkland fluid resuscitation (burn) ─────────────────────
  // 4 mL × weight(kg) × %TBSA over 24 h; half in first 8 h.
  function parkland(weightKg, tbsaPct) {
    if (!weightKg || weightKg <= 0 || !tbsaPct || tbsaPct <= 0) return null;
    var total = 4 * weightKg * tbsaPct;
    var first8 = total / 2;
    var next16 = total - first8;
    return {
      totalMl: Math.round(total),
      first8Ml: Math.round(first8),
      next16Ml: Math.round(next16),
      rateFirst8: Math.round(first8 / 8),
      rateNext16: Math.round(next16 / 16)
    };
  }

  // ── Maintenance fluids (Holliday-Segar 4-2-1 rule) ─────────
  function maintenance421(weightKg) {
    if (!weightKg || weightKg <= 0) return null;
    if (weightKg <= 10) return Math.round(weightKg * 4);
    if (weightKg <= 20) return Math.round(40 + (weightKg - 10) * 2);
    return Math.round(60 + (weightKg - 20));
  }

  // ── PRAM score (asthma, 0-12) ───────────────────────────────
  function pramScore(spo2, retractions, scalene, airEntry, wheeze) {
    var total = [spo2, retractions, scalene, airEntry, wheeze]
      .reduce(function(s, v) { return s + (parseInt(v, 10) || 0); }, 0);
    var severity = total <= 3 ? 'Mild' : total <= 7 ? 'Moderate' : 'Severe';
    return { total: total, severity: severity };
  }

  // ── Westley croup score (0-17) ──────────────────────────────
  function westleyScore(conscious, cyanosis, stridor, airEntry, retractions) {
    var total = [conscious, cyanosis, stridor, airEntry, retractions]
      .reduce(function(s, v) { return s + (parseInt(v, 10) || 0); }, 0);
    var severity;
    if (total <= 2) severity = 'Mild';
    else if (total <= 5) severity = 'Moderate';
    else if (total <= 11) severity = 'Severe';
    else severity = 'Impending respiratory failure';
    return { total: total, severity: severity };
  }

  // ── Epinephrine: anaphylaxis IM vs arrest IV ────────────────
  // IMPORTANT: different concentrations (1:1,000 vs 1:10,000).
  function epiAnaphylaxisIM(weightKg) {
    if (!weightKg || weightKg <= 0) return null;
    var mg = Math.min(weightKg * 0.01, 0.5);
    return { doseMg: roundTo(mg, 2), volumeMl_1in1000: roundTo(mg, 2) };
  }
  function epiArrestIV(weightKg) {
    if (!weightKg || weightKg <= 0) return null;
    var mg = Math.min(weightKg * 0.01, 1);
    return { doseMg: roundTo(mg, 2), volumeMl_1in10000: roundTo(mg * 10, 2) };
  }

  // ── NRP epinephrine (neonatal resus) ────────────────────────
  // IV/IO 0.01-0.03 mg/kg = 0.1-0.3 mL/kg of 1:10,000.
  function nrpEpiIV(weightKg) {
    if (!weightKg || weightKg <= 0) return null;
    return {
      lowMg: roundTo(weightKg * 0.01, 2),
      highMg: roundTo(weightKg * 0.03, 2),
      lowMl: roundTo(weightKg * 0.1, 2),
      highMl: roundTo(weightKg * 0.3, 2)
    };
  }

  // ── RSI drug doses (selected) ───────────────────────────────
  function rsiKetamine(weightKg) {
    if (!weightKg) return null;
    return { lowMg: Math.min(roundTo(weightKg * 1.5, 1), 150), highMg: Math.min(roundTo(weightKg * 2, 1), 200) };
  }
  function rsiRocuronium(weightKg) {
    if (!weightKg) return null;
    return { mg: Math.min(roundTo(weightKg * 1.2, 1), 100) };
  }
  function rsiSuccinylcholine(weightKg) {
    if (!weightKg) return null;
    var perKg = weightKg < 10 ? 2 : 1.5;
    return { mg: Math.min(roundTo(weightKg * perKg, 1), 150), perKg: perKg };
  }

  // ── Minimum acceptable systolic BP (1-10 yr) ────────────────
  function minSBP(years) {
    if (years == null || isNaN(years)) return null;
    if (years < 1) return 70;
    if (years > 10) return 90;
    return 70 + 2 * years;
  }

  // ── ETT (endotracheal tube) sizing by age ───────────────────
  function ettSize(ageYears) {
    if (ageYears == null || ageYears < 0) return null;
    var uncuffed = ageYears / 4 + 4;
    var cuffed = ageYears / 4 + 3.5;
    return {
      uncuffedMm: roundTo(uncuffed, 1),
      cuffedMm: roundTo(cuffed, 1),
      depthCm: roundTo(uncuffed * 3, 1)
    };
  }

  // ── Lund-Browder regional TBSA by age bracket ───────────────
  // Used for burns TBSA calculation. Bracket: 'infant', 'young',
  // 'child', 'adol', 'adult' — per 0-1, 1-5, 5-10, 10-15, >15 yr.
  var LB_BRACKETS = ['infant', 'young', 'child', 'adol', 'adult'];
  var LB = {
    head:       [18,   13,  11,  9,   7],
    neck:       [2,    2,   2,   2,   2],
    ant_trunk:  [13,   13,  13,  13,  13],
    post_trunk: [13,   13,  13,  13,  13],
    r_buttock:  [2.5,  2.5, 2.5, 2.5, 2.5],
    l_buttock:  [2.5,  2.5, 2.5, 2.5, 2.5],
    genital:    [1,    1,   1,   1,   1],
    r_uparm:    [4,    4,   4,   4,   4],
    l_uparm:    [4,    4,   4,   4,   4],
    r_forearm:  [3,    3,   3,   3,   3],
    l_forearm:  [3,    3,   3,   3,   3],
    r_hand:     [2.5,  2.5, 2.5, 2.5, 2.5],
    l_hand:     [2.5,  2.5, 2.5, 2.5, 2.5],
    r_thigh:    [5.5,  8,   8.5, 9,   9.5],
    l_thigh:    [5.5,  8,   8.5, 9,   9.5],
    r_leg:      [5,    5.5, 6,   6.5, 7],
    l_leg:      [5,    5.5, 6,   6.5, 7],
    r_foot:     [3.5,  3.5, 3.5, 3.5, 3.5],
    l_foot:     [3.5,  3.5, 3.5, 3.5, 3.5]
  };

  // involvements: { regionKey: percentOfRegionInvolved (0-100), ... }
  function lundBrowderTBSA(ageBracket, involvements) {
    var idx = LB_BRACKETS.indexOf(ageBracket);
    if (idx < 0) return null;
    var total = 0;
    Object.keys(LB).forEach(function(key) {
      var pct = (involvements && involvements[key]) || 0;
      if (pct < 0) pct = 0; if (pct > 100) pct = 100;
      total += LB[key][idx] * (pct / 100);
    });
    return roundTo(total, 1);
  }

  var API = {
    roundTo: roundTo,
    estimateWeightFromAgeMonths: estimateWeightFromAgeMonths,
    parkland: parkland,
    maintenance421: maintenance421,
    pramScore: pramScore,
    westleyScore: westleyScore,
    epiAnaphylaxisIM: epiAnaphylaxisIM,
    epiArrestIV: epiArrestIV,
    nrpEpiIV: nrpEpiIV,
    rsiKetamine: rsiKetamine,
    rsiRocuronium: rsiRocuronium,
    rsiSuccinylcholine: rsiSuccinylcholine,
    minSBP: minSBP,
    ettSize: ettSize,
    lundBrowderTBSA: lundBrowderTBSA,
    LB_BRACKETS: LB_BRACKETS,
    LB: LB
  };

  if (typeof window !== 'undefined') window._PED_MATH = API;
  if (typeof module !== 'undefined' && module.exports) module.exports = API;
})();
