// payout-sankey.jsx
// Shared Sankey-style flow used by several treatments.
// 3 columns: Policies → Beneficiary → Expenses
// Supports per-beneficiary focus (filter to one) or full ("all").
// Each ribbon is one (policy → expense) flow, tinted by beneficiary.

const PGSankey = ({
  policies = PG_POLICIES,
  expenses,            // [{ id, label, to, amount, horizon, category }]
  focus = "all",       // "all" | "David" | "Eli" | "Maya"
  width = 1100,
  height = 540,
  hoverExpense = null,
  onExpenseHover = () => {},
  onExpenseClick = () => {},
  showHeaders = true,
  compactExpense = false,
}) => {
  const W = width, H = height;
  const headerH = showHeaders ? 22 : 0;
  const topPad = headerH + 8;
  const botPad = 16;
  const usableH = H - topPad - botPad;

  // Beneficiary palette — identical DNA to the coverage-map Sankey so the two
  // visualizations read as one system. Each beneficiary owns a single hue, and
  // ribbons fade from a pale cream at the policy source into that hue.
  const BENE_PALETTE = [
    { light: "#FFE4CB", base: "#FFB876", deep: "#D38D54" }, // peach/orange
    { light: "#CFEDF1", base: "#A1DCE2", deep: "#70B6BA" }, // sky blue
    { light: "#E5F0AE", base: "#C0C983", deep: "#8D9933" }, // soft chartreuse
    { light: "#E2D7F4", base: "#C6B3EB", deep: "#9888BF" }, // soft purple
    { light: "#FFD6D6", base: "#E89B9B", deep: "#B36868" }, // soft rose
    { light: "#D9E4D4", base: "#A8BD9F", deep: "#6F8862" }, // sage
  ];
  const PG_BENE_ORDER = (PG_PEOPLE || []).map(p => p.id);
  const colorForBene = (id) => {
    const i = PG_BENE_ORDER.indexOf(id);
    return BENE_PALETTE[(i < 0 ? 0 : i) % BENE_PALETTE.length];
  };
  const MISSING = { light: "#F2C6B3", base: "#E89B7D", deep: "#D38D54" }; // coral
  const GROWTH  = { light: "#E5F0AE", base: "#C0C983", deep: "#8D9933" }; // chartreuse
  const ECRU = "#F0E9CB"; // pale cream at the policy source

  // Filter to focus
  const visiblePeople = focus === "all" ? PG_PEOPLE : PG_PEOPLE.filter(p => p.id === focus);
  const visibleExpenses = focus === "all" ? expenses : expenses.filter(e => e.to === focus);

  // Per-beneficiary share = when over-allocated, show NEEDED amount (allocated);
  // when under-allocated, show existing policy total
  const beneTotals = {};
  const missingCoveragePolicies = []; // Calculate early, needed for polTotals
  
  visiblePeople.forEach(p => {
    const fromPolicies = PG_TOTAL_BY[p.id] || 0;
    const allocated = visibleExpenses.filter(e => e.to === p.id).reduce((s, e) => s + e.amount, 0);
    
    // Share shows the LARGER of the two (what's needed to cover everything)
    beneTotals[p.id] = Math.max(fromPolicies, allocated);
    
    // Track missing coverage as a policy if over-allocated
    const diff = fromPolicies - allocated;
    if (diff < 0) {
      missingCoveragePolicies.push({
        id: `__missing_${p.id}`,
        personId: p.id,
        amount: Math.abs(diff)
      });
    }
  });

  // Per-policy totals (including missing coverage policies on the LEFT)
  const polTotals = {};
  policies.forEach(pol => {
    let t = 0;
    Object.entries(pol.splits).forEach(([who, pct]) => {
      if (focus === "all" || who === focus) t += pol.faceAmount * pct;
    });
    if (t > 0) polTotals[pol.id] = t;
  });
  
  // Add missing coverage policies to polTotals (will be added to polPos later)
  missingCoveragePolicies.forEach(item => {
    polTotals[item.id] = item.amount;
  });

  // Grand total drives the $-to-px scale
  const grandTotal = Object.values(polTotals).reduce((a, b) => a + b, 0) || 1;

  const colX = {
    polStart: 0,     polEnd: 100,
    beneStart: 220,  beneEnd: 300,
    expStart: 500,   expEnd: W - 6,
  };

  const GAP = 6;
  const HORIZON_GAP = 20; // visual break between horizon groups in the expense column

  // ── Stack columns vertically, summing to usableH ──
  // gapFn(prevKey, currKey) → px lets a column use bigger gaps at group breaks.
  const stackCol = (totalsObj, keys, gapFn) => {
    const entries = keys.map(k => [k, totalsObj[k] || 0]).filter(([, v]) => v > 0);
    const sum = entries.reduce((a, [, v]) => a + v, 0) || 1;
    // Total whitespace eaten by gaps
    let gapPxTotal = 0;
    const gaps = entries.map(([k], i) => {
      if (i === 0) return 0;
      const g = gapFn ? gapFn(entries[i - 1][0], k) : GAP;
      gapPxTotal += g;
      return g;
    });
    const drawH = Math.max(0, usableH - gapPxTotal);
    const m = new Map();
    let y = topPad;
    entries.forEach(([k, v], i) => {
      y += gaps[i];
      const h = (v / sum) * drawH;
      m.set(k, { y, h, v });
      y += h;
    });
    return m;
  };

  const polKeys = [...policies.map(p => p.id), ...missingCoveragePolicies.map(m => m.id)].filter(k => polTotals[k]);
  const polPos = stackCol(polTotals, polKeys);

  // Bene column: people only (no unallocated remainders - share is sized to what's needed)
  const beneKeys = visiblePeople.map(p => p.id).filter(k => beneTotals[k]);
  const benePos = stackCol(beneTotals, beneKeys);  // Expense column: PRIMARILY grouped by horizon (Now → Soon → Later),
  // then by beneficiary, then by size. This lets readers see "what's
  // due now" all in one block, and "the long arc" together.
  const horizonOrder = { now: 0, near: 1, long: 2 };
  const expensesSorted = [...visibleExpenses]
    .sort((a, b) => {
      const ah = horizonOrder[a.horizon] ?? 99;
      const bh = horizonOrder[b.horizon] ?? 99;
      if (ah !== bh) return ah - bh;
      const av = visiblePeople.findIndex(p => p.id === a.to);
      const bv = visiblePeople.findIndex(p => p.id === b.to);
      if (av !== bv) return av - bv;
      return b.amount - a.amount;
    });

  // Track if there's under-allocation (invest for growth)
  // (missing coverage is already tracked above in missingCoveragePolicies)
  let expColEntries = expensesSorted.map(e => [e.id, e.amount]);
  
  visiblePeople.forEach(p => {
    const share = PG_TOTAL_BY[p.id];
    const allocated = visibleExpenses.filter(e => e.to === p.id).reduce((s, e) => s + e.amount, 0);
    const diff = share - allocated;
    
    if (diff > 0) {
      // Under-allocated: add green "invest for growth" purpose
      expColEntries.push([`__growth_${p.id}`, diff]);
    }
    // Over-allocation already handled above as missingCoveragePolicies
  });
  
  const expTotalsObj = Object.fromEntries(expColEntries);
  const expKeys = expColEntries.map(([k]) => k);
  
  // Horizon lookup for the gap function
  const expHorizon = (key) => {
    if (key.startsWith("__growth_")) return "_growth";
    const e = visibleExpenses.find(x => x.id === key);
    return e ? e.horizon : null;
  };
  const expPos = stackCol(expTotalsObj, expKeys, (prev, curr) =>
    expHorizon(prev) !== expHorizon(curr) ? HORIZON_GAP : GAP
  );

  // Build ribbons in TWO independent sets:
  // Set A: Policy → Share (one ribbon per policy split to each beneficiary)
  // Set B: Share → Purpose (one ribbon per expense from each beneficiary share)
  // This eliminates criss-crossing because we don't try to trace individual
  // policy dollars all the way through to individual purposes.
  
  const ribbonsPolToShare = [];
  const ribbonsShareToExp = [];
  
  // Set A: Policy → Share connections (including missing coverage policies)
  policies.forEach(pol => {
    Object.entries(pol.splits).forEach(([who, pct]) => {
      if (focus !== "all" && who !== focus) return;
      const amount = pol.faceAmount * pct;
      if (amount > 0) {
        ribbonsPolToShare.push({
          polId: pol.id,
          beneId: who,
          amount,
          type: 'pol-to-share',
        });
      }
    });
  });
  
  // Add missing coverage policies → share flows
  missingCoveragePolicies.forEach(item => {
    ribbonsPolToShare.push({
      polId: item.id,
      beneId: item.personId,
      amount: item.amount,
      type: 'pol-to-share',
      missing: true, // Flag for red/warning styling
    });
  });
  
  // Set B: Share → Purpose connections
  visiblePeople.forEach(person => {
    const share = PG_TOTAL_BY[person.id];
    const personExpenses = visibleExpenses.filter(e => e.to === person.id);
    const allocated = personExpenses.reduce((s, e) => s + e.amount, 0);
    const diff = share - allocated;
    
    // Add regular expense flows (actual amounts)
    personExpenses.forEach(exp => {
      ribbonsShareToExp.push({
        beneId: exp.to,
        expId: exp.id,
        amount: exp.amount,
        horizon: exp.horizon,
        type: 'share-to-exp',
      });
    });
    
    // Only add growth flow if under-allocated (missing coverage is now on the LEFT as a policy)
    if (diff > 0) {
      ribbonsShareToExp.push({
        beneId: person.id,
        expId: `__growth_${person.id}`,
        amount: diff,
        growth: true,
        type: 'share-to-exp',
      });
    }
  });

  // Compute y-offsets for Policy → Share ribbons (Set A)
  const polOut = new Map();
  const beneInFromPol = new Map();
  
  ribbonsPolToShare.forEach(rb => {
    const pPos = polPos.get(rb.polId);
    const bPos = benePos.get(rb.beneId);
    if (!pPos || !bPos) { rb.skip = true; return; }
    
    // Policy output side
    const polOff = polOut.get(rb.polId) || 0;
    rb.h_pol = (rb.amount / pPos.v) * pPos.h;
    rb.yPolA = pPos.y + polOff;
    rb.yPolB = rb.yPolA + rb.h_pol;
    polOut.set(rb.polId, polOff + rb.h_pol);
    
    // Share input side
    const beneOff = beneInFromPol.get(rb.beneId) || 0;
    rb.h_bene = (rb.amount / bPos.v) * bPos.h;
    rb.yBeneA = bPos.y + beneOff;
    rb.yBeneB = rb.yBeneA + rb.h_bene;
    beneInFromPol.set(rb.beneId, beneOff + rb.h_bene);
  });
  
  // Compute y-offsets for Share → Purpose ribbons (Set B)
  // Sort by expense order first to eliminate criss-crossing
  ribbonsShareToExp.sort((a, b) => {
    const ea = expKeys.indexOf(a.expId), eb = expKeys.indexOf(b.expId);
    return ea - eb;
  });
  
  const beneOutToPurpose = new Map();
  const expIn = new Map();
  
  ribbonsShareToExp.forEach(rb => {
    const bPos = benePos.get(rb.beneId);
    const ePos = expPos.get(rb.expId);
    if (!bPos || !ePos) { rb.skip = true; return; }
    
    // Share output side
    let beneOff = beneOutToPurpose.get(rb.beneId) || 0;
    
    // For missing coverage, DON'T increment the share offset - it shows deficit beyond share
    // The ribbon should start at the END of the share (where normal flows stop)
    const isMissing = rb.missing || false;
    
    rb.h_bene = (rb.amount / bPos.v) * bPos.h;
    rb.yBeneA = bPos.y + beneOff;
    rb.yBeneB = rb.yBeneA + rb.h_bene;
    
    // Only increment offset for non-missing flows
    if (!isMissing) {
      beneOutToPurpose.set(rb.beneId, beneOff + rb.h_bene);
    }
    
    // Purpose input side
    const expOff = expIn.get(rb.expId) || 0;
    rb.h_exp = (rb.amount / ePos.v) * ePos.h;
    rb.yExpA = ePos.y + expOff;
    rb.yExpB = rb.yExpA + rb.h_exp;
    expIn.set(rb.expId, expOff + rb.h_exp);
  });

  const ribbonPath = (xS, yA1, yB1, xE, yA2, yB2) => {
    const cx = (xS + xE) / 2;
    return `M${xS},${yA1} C${cx},${yA1} ${cx},${yA2} ${xE},${yA2} L${xE},${yB2} C${cx},${yB2} ${cx},${yB1} ${xS},${yB1} Z`;
  };

  // Color is now driven entirely by beneficiary (see BENE_PALETTE above), so
  // the payout-guidance flow matches the coverage-map Sankey one-to-one. The
  // old horizon-based ribbon coloring has been retired — horizon still reads
  // from each purpose's label ("· Now / · Soon / · Later").

  // Format a $ amount in full
  const pgFull = (v) => `$${Math.round(v).toLocaleString()}`;

  const headerStyle = { fontFamily: "var(--font-sans)", fontSize: 10, fontWeight: 700,
    fill: "var(--helm-dark-grey)", textTransform: "uppercase", letterSpacing: "0.08em" };
  const labelStyle = { fontFamily: "var(--font-sans)", fontSize: 12, fontWeight: 600, fill: "var(--helm-dark-green)" };
  const subStyle = { fontFamily: "var(--font-sans)", fontSize: 11, fontWeight: 500, fill: "var(--helm-dark-green)", opacity: 0.65 };

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%"
      style={{ display: "block", overflow: "visible", fontFamily: "var(--font-sans)" }}>
      <defs>
        {/* Policy → Share: one gradient per ribbon, pale cream (or coral for
            missing coverage) fading into that beneficiary's hue. Mirrors the
            coverage-map seg1. */}
        {ribbonsPolToShare.filter(r => !r.skip).map((rb, i) => {
          const tint = colorForBene(rb.beneId);
          const start = rb.missing ? MISSING.base : ECRU;
          return (
            <linearGradient key={`gp-${i}`} id={`pg-pts-${i}`}
              x1={colX.polEnd} x2={colX.beneStart} y1="0" y2="0" gradientUnits="userSpaceOnUse">
              <stop offset="0%"   stopColor={start}     stopOpacity="0.92" />
              <stop offset="100%" stopColor={tint.base} stopOpacity="0.9" />
            </linearGradient>
          );
        })}

        {/* Share → Purpose: solid beneficiary hue (chartreuse for the synthetic
            "invest for growth" bucket). Mirrors the coverage-map seg2. */}
        {ribbonsShareToExp.filter(r => !r.skip).map((rb, i) => {
          const tint = colorForBene(rb.beneId);
          const end = rb.growth ? GROWTH.base : tint.base;
          return (
            <linearGradient key={`gs-${i}`} id={`pg-ste-${i}`}
              x1={colX.beneStart} x2={colX.expStart} y1="0" y2="0" gradientUnits="userSpaceOnUse">
              <stop offset="0%"   stopColor={tint.base} stopOpacity="0.92" />
              <stop offset="100%" stopColor={end}       stopOpacity="0.9" />
            </linearGradient>
          );
        })}
      </defs>

      {showHeaders && (
        <>
          <text x={(colX.polStart + colX.polEnd) / 2} y={14} textAnchor="middle" style={headerStyle}>Policies</text>
          <text x={(colX.beneStart + colX.beneEnd) / 2} y={14} textAnchor="middle" style={headerStyle}>{focus === "all" ? "Beneficiaries" : "Share"}</text>
          <text x={(colX.expStart + colX.expEnd) / 2} y={14} textAnchor="middle" style={headerStyle}>Purpose</text>
        </>
      )}

      {/* Policy → Share ribbons (Set A) */}
      <g>
        {ribbonsPolToShare.filter(r => !r.skip).map((rb, i) => (
          <path key={`pol-share-${i}`}
            d={ribbonPath(colX.polEnd, rb.yPolA, rb.yPolB, colX.beneStart, rb.yBeneA, rb.yBeneB)}
            fill={`url(#pg-pts-${i})`} />
        ))}
      </g>

      {/* Share → Purpose ribbons (Set B) */}
      <g>
        {ribbonsShareToExp.filter(r => !r.skip).map((rb, i) => {
          const isHover = hoverExpense && rb.expId === hoverExpense;
          return (
            <path key={`share-exp-${i}`}
              d={ribbonPath(colX.beneStart, rb.yBeneA, rb.yBeneB, colX.expStart, rb.yExpA, rb.yExpB)}
              fill={`url(#pg-ste-${i})`}
              opacity={hoverExpense ? (isHover ? 1 : 0.25) : 1}
              style={{ transition: "opacity .15s" }}
            />
          );
        })}
      </g>

      {/* Policy nodes — pale cream to match the gradient start */}
      {policies.filter(p => polTotals[p.id]).map(pol => {
        const p = polPos.get(pol.id);
        return (
          <g key={`pol-${pol.id}`}>
            <rect x={colX.polStart} y={p.y} width={colX.polEnd - colX.polStart}
              height={p.h} fill="#F0E9CB" fillOpacity={0.95} rx={4} />
            <text x={colX.polStart + 10} y={p.y + 16} style={labelStyle}>{pol.carrier.split(" ")[0]}</text>
            {p.h > 32 && <text x={colX.polStart + 10} y={p.y + 32} style={subStyle}>{pgFull(p.v)}</text>}
          </g>
        );
      })}
      
      {/* Missing coverage policies — red/warning style to indicate needed purchase */}
      {missingCoveragePolicies.map(item => {
        const p = polPos.get(item.id);
        if (!p) return null;
        return (
          <g key={`pol-${item.id}`}>
            <rect x={colX.polStart} y={p.y} width={colX.polEnd - colX.polStart}
              height={p.h} fill="#E89B7D" fillOpacity={0.95} rx={4} />
            <text x={colX.polStart + 10} y={p.y + 16} style={labelStyle}>Missing coverage</text>
            {p.h > 32 && <text x={colX.polStart + 10} y={p.y + 32} style={subStyle}>{pgFull(p.v)}</text>}
          </g>
        );
      })}

      {/* Bene nodes — tinted with this beneficiary's hue */}
      {visiblePeople.map(person => {
        const p = benePos.get(person.id);
        if (!p) return null;
        const c = colorForBene(person.id);
        return (
          <g key={`bn-${person.id}`}>
            <rect x={colX.beneStart} y={p.y} width={colX.beneEnd - colX.beneStart}
              height={p.h} fill={c.base} fillOpacity={0.9} />
            <text x={colX.beneStart + 10} y={p.y + 16} style={labelStyle}>{person.name.split(" ")[0]}</text>
            {p.h > 32 && <text x={colX.beneStart + 10} y={p.y + 32} style={subStyle}>{pgFull(p.v)}</text>}
          </g>
        );
      })}


      {/* Expense nodes — labels INSIDE the rectangle, tinted by beneficiary, sharp left edge */}
      {expensesSorted.map(exp => {
        const p = expPos.get(exp.id);
        if (!p) return null;
        const bgColor = colorForBene(exp.to).base;
        const isHover = hoverExpense === exp.id;
        // Always use dark text for readability
        const textColor = "#001D00";
        return (
          <g key={`exp-${exp.id}`}
            onMouseEnter={() => onExpenseHover(exp.id)}
            onMouseLeave={() => onExpenseHover(null)}
            onClick={() => onExpenseClick(exp.id)}
            style={{ cursor: "pointer" }}>
            <rect x={colX.expStart} y={p.y} width={colX.expEnd - colX.expStart}
              height={p.h} fill={bgColor} fillOpacity={isHover ? 1 : 0.85} rx={0} />
            <text x={colX.expStart + 10} y={p.y + 15} style={{...labelStyle, fill: textColor}}>{exp.label}</text>
            {p.h > 30 && <text x={colX.expStart + 10} y={p.y + 30} style={{...subStyle, fill: textColor, opacity: 0.65}}>
              {pgFull(p.v)} · {(PG_HORIZONS.find(h => h.id === exp.horizon) || {}).short}
            </text>}
          </g>
        );
      })}

      {/* Invest-for-growth expense bucket */}
      {visiblePeople.map(person => {
        const k = `__growth_${person.id}`;
        const pos = expPos.get(k);
        if (!pos) return null;
        return (
          <g key={`exp-growth-${person.id}`}>
            <rect x={colX.expStart} y={pos.y} width={colX.expEnd - colX.expStart}
              height={pos.h} fill="#C0C983" fillOpacity={0.9} rx={0} />
            <text x={colX.expStart + 10} y={pos.y + 15} style={{...labelStyle, fill: "#001D00"}}>
              Invest for growth
            </text>
            {pos.h > 30 && <text x={colX.expStart + 10} y={pos.y + 30} style={{...subStyle, fill: "#001D00", opacity: 0.65}}>
              {pgFull(pos.v)}
            </text>}
          </g>
        );
      })}
      
      {/* Missing coverage expense bucket */}
      {visiblePeople.map(person => {
        const k = `__missing_${person.id}`;
        const pos = expPos.get(k);
        if (!pos) return null;
        return (
          <g key={`exp-missing-${person.id}`}>
            <rect x={colX.expStart} y={pos.y} width={colX.expEnd - colX.expStart}
              height={pos.h} fill="#D38D54" fillOpacity={0.92} rx={0} />
            <text x={colX.expStart + 10} y={pos.y + 15} style={{...labelStyle, fill: "#FEFCF5"}}>
              Missing coverage
            </text>
            {pos.h > 30 && <text x={colX.expStart + 10} y={pos.y + 30} style={{...subStyle, fill: "#FEFCF5", opacity: 0.85}}>
              {pgFull(pos.v)}
            </text>}
          </g>
        );
      })}
    </svg>
  );
};

Object.assign(window, { PGSankey });
