// financial/src/kids.jsx — the Kids section.
//
// Kid profiles, 529/UTMA account assignment, the deterministic college
// trajectory math + CollegeTrajectoryCard, the household posture checklist,
// and the AI plan (generateKidsPlan) rendered through MarkdownView.
//
// Slices run in their own Babel scope; src/shared.jsx runs first and
// publishes window.FinanceShared.

const { useState, useEffect, useContext, useMemo, useRef, useCallback } = React;
const { ResponsiveContainer, LineChart, Line, BarChart, Bar, AreaChart, Area,
        ComposedChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine } = window.Recharts;
const { DataContext, useToast, callFn, fmtCur, calcAge, MarkdownView } = window.FinanceShared;

// ── College trajectory math (deterministic, used by CollegeTrajectoryCard) ─
// Sticker-price all-in COA for 2025-26, projected forward at 5%/yr.
// Returns are in real terms (6%) so we can compare to inflated COA cleanly.
const COLLEGE_SCENARIOS_2025 = [
  { key: 'udelInState',         label: 'UDel (in-state)',           annualCOA: 34000 },
  { key: 'outOfStatePublicAvg', label: 'Out-of-state public avg',   annualCOA: 46500 },
  { key: 'udelOutOfState',      label: 'UDel (out-of-state)',       annualCOA: 58000 },
  { key: 'privateAvg',          label: 'Private 4-year avg',        annualCOA: 62000 },
];
const REAL_RETURN_PCT  = 6;
const REAL_RETURN_LOW  = 4;   // conservative scenario
const REAL_RETURN_HIGH = 8;   // optimistic scenario
const EDU_INFLATION_PCT = 5;

// FV at month n given a real-return rate (% per year, monthly compounding).
function fvAtMonth(currentBalance, monthlyContrib, monthsElapsed, ratePct) {
  const r = ratePct / 100 / 12;
  const n = Math.max(0, monthsElapsed);
  if (n === 0) return currentBalance;
  if (r === 0) return currentBalance + monthlyContrib * n;
  return currentBalance * Math.pow(1 + r, n) + monthlyContrib * (Math.pow(1 + r, n) - 1) / r;
}

// FV of a balance + monthly contributions, monthly compounding, at base rate.
function projectSavings(currentBalance, monthlyContrib, years) {
  return fvAtMonth(currentBalance, monthlyContrib, Math.round(years * 12), REAL_RETURN_PCT);
}

// Year-by-year balance path (from t=0 to t=yearsToCollege), one entry per year,
// at low/mean/high rates. Used to render the fan chart.
function projectionPath(currentBalance, monthlyContrib, yearsToCollege, currentYear) {
  const yrs = Math.max(0, Math.round(yearsToCollege));
  const out = [];
  for (let y = 0; y <= yrs; y++) {
    const months = y * 12;
    const low  = Math.round(fvAtMonth(currentBalance, monthlyContrib, months, REAL_RETURN_LOW));
    const mean = Math.round(fvAtMonth(currentBalance, monthlyContrib, months, REAL_RETURN_PCT));
    const high = Math.round(fvAtMonth(currentBalance, monthlyContrib, months, REAL_RETURN_HIGH));
    out.push({
      year: currentYear + y,
      yearsOut: y,
      low, mean, high,
      range: [low, high],  // Recharts area-range tuple
    });
  }
  return out;
}

// Sum of inflated COA across all 4 college years, starting at year-of-matriculation.
function inflated4YearCOA(annualCOA2025, yearsToCollege) {
  const e = EDU_INFLATION_PCT / 100;
  let total = 0;
  for (let i = 0; i < 4; i++) {
    total += annualCOA2025 * Math.pow(1 + e, yearsToCollege + i);
  }
  return total;
}

// Solve for monthly contribution that makes FV equal target.
function requiredMonthly(currentBalance, target, years) {
  const r = REAL_RETURN_PCT / 100 / 12;
  const n = Math.max(0, Math.round(years * 12));
  if (n === 0) return Math.max(0, target - currentBalance);
  const fvLump = currentBalance * Math.pow(1 + r, n);
  const annFactor = (Math.pow(1 + r, n) - 1) / r;
  const pmt = (target - fvLump) / annFactor;
  return Math.max(0, pmt);
}

const fmtMoney = (n) => {
  if (!isFinite(n)) return '—';
  if (Math.abs(n) >= 1000) return '$' + Math.round(n / 1000).toLocaleString() + 'k';
  return '$' + Math.round(n).toLocaleString();
};
const fmtMoneyExact = (n) => '$' + Math.round(n).toLocaleString();

function CollegeTrajectoryCard({ kids, kidAccounts, assignments, monthlyByAccount, currentYear }) {
  const calc = (() => {
    return kids.filter(k => k.name && k.dob).map(k => {
      const dob = new Date(k.dob);
      const ageMs = Date.now() - dob.getTime();
      const age = ageMs / (365.25 * 24 * 3600 * 1000);
      const collegeStart = k.collegeStartYear ? Number(k.collegeStartYear) : (dob.getFullYear() + 18);
      const yearsToCollege = Math.max(0, collegeStart - currentYear);
      const accts = kidAccounts.filter(a => assignments[a.id] === k.id);
      const balance = accts.reduce((s, a) => s + (a.currentBalance || 0), 0);
      const monthly = accts.reduce((s, a) => s + (Number(monthlyByAccount[a.id]) || 0), 0);
      const projected = projectSavings(balance, monthly, yearsToCollege);
      const scenarios = COLLEGE_SCENARIOS_2025.map(s => {
        const cost = inflated4YearCOA(s.annualCOA, yearsToCollege);
        const gap = cost - projected;
        const monthlyNeeded = requiredMonthly(balance, cost, yearsToCollege);
        return { ...s, cost, gap, monthlyNeeded };
      });
      const path = projectionPath(balance, monthly, yearsToCollege, currentYear);
      // Endpoint values (low/mean/high) at college start, plus cost markers
      const endpoint = path[path.length - 1] || { low: balance, mean: balance, high: balance };
      const udelCOA = scenarios.find(s => s.key === 'udelInState')?.cost || 0;
      const oosCOA  = scenarios.find(s => s.key === 'outOfStatePublicAvg')?.cost || 0;
      return { id: k.id, name: k.name, age: Math.floor(age), yearsToCollege, collegeStart, balance, monthly, projected, scenarios, path, endpoint, udelCOA, oosCOA };
    });
  })();

  if (calc.length === 0) return null;

  const yAxisFmt = (v) => v >= 1000 ? '$' + Math.round(v/1000) + 'k' : '$' + v;

  return (
    <div className="card" style={{marginBottom:16}}>
      <div style={{marginBottom:14}}>
        <div style={{fontWeight:600,fontSize:14}}>College Trajectory</div>
        <div style={{fontSize:11,color:'#64748b',marginTop:2}}>
          Shaded band = 4%–8% real return range; line = 6% real (S&P-style ETFs). Cost lines = 4-yr sticker COA at matriculation, inflated at {EDU_INFLATION_PCT}%/yr.
        </div>
      </div>
      <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(420px,1fr))',gap:14}}>
        {calc.map(k => (
          <div key={k.id} style={{border:'1px solid #1e293b',borderRadius:8,padding:'12px 14px',background:'#0b1220'}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',marginBottom:8}}>
              <div style={{fontWeight:600,fontSize:13}}>{k.name}</div>
              <div style={{fontSize:11,color:'#64748b'}}>age {k.age} · college {k.collegeStart} ({k.yearsToCollege} yrs)</div>
            </div>
            <div style={{display:'flex',gap:14,fontSize:11,color:'#94a3b8',marginBottom:8,flexWrap:'wrap'}}>
              <div>now <span style={{color:'#e2e8f0',fontWeight:600}}>{fmtMoneyExact(k.balance)}</span></div>
              <div>+{fmtMoneyExact(k.monthly)}/mo</div>
              <div>at start: <span style={{color:'#f87171',fontWeight:600}}>{fmtMoney(k.endpoint.low)}</span> – <span style={{color:'#10b981',fontWeight:600}}>{fmtMoney(k.endpoint.mean)}</span> – <span style={{color:'#34d399',fontWeight:600}}>{fmtMoney(k.endpoint.high)}</span></div>
            </div>
            <div style={{height:200,marginBottom:10}}>
              <ResponsiveContainer width="100%" height="100%">
                <ComposedChart data={k.path} margin={{top:4,right:8,left:0,bottom:0}}>
                  <defs>
                    <linearGradient id={`gFan-${k.id}`} x1="0" y1="0" x2="0" y2="1">
                      <stop offset="0%"   stopColor="#10b981" stopOpacity={0.30}/>
                      <stop offset="100%" stopColor="#10b981" stopOpacity={0.05}/>
                    </linearGradient>
                  </defs>
                  <CartesianGrid stroke="#1e293b" strokeDasharray="2 4"/>
                  <XAxis dataKey="year" tick={{fontSize:10,fill:'#64748b'}} axisLine={{stroke:'#1e293b'}} tickLine={false}/>
                  <YAxis tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false} tickFormatter={yAxisFmt} width={50}/>
                  <Tooltip
                    contentStyle={{background:'#020617',border:'1px solid #1e293b',borderRadius:6,fontSize:11}}
                    labelStyle={{color:'#94a3b8'}}
                    itemStyle={{color:'#e2e8f0'}}
                    formatter={(value, name) => {
                      if (name === 'range' && Array.isArray(value)) return [`${fmtMoneyExact(value[0])} – ${fmtMoneyExact(value[1])}`, '4–8% real'];
                      if (name === 'mean') return [fmtMoneyExact(value), '6% real'];
                      return [fmtMoneyExact(value), name];
                    }}
                  />
                  <Area type="monotone" dataKey="range" stroke="none" fill={`url(#gFan-${k.id})`} dot={false} name="range" isAnimationActive={false}/>
                  <Line type="monotone" dataKey="mean" stroke="#10b981" strokeWidth={2.5} dot={false} name="mean"/>
                  {k.udelCOA > 0 && <ReferenceLine y={k.udelCOA} stroke="#3b82f6" strokeDasharray="4 4" strokeWidth={1.5} label={{value:`UDel ${fmtMoney(k.udelCOA)}`, position:'insideTopLeft', fill:'#3b82f6', fontSize:10}}/>}
                  {k.oosCOA > 0 && <ReferenceLine y={k.oosCOA}  stroke="#f59e0b" strokeDasharray="4 4" strokeWidth={1.5} label={{value:`OOS ${fmtMoney(k.oosCOA)}`,  position:'insideBottomLeft', fill:'#f59e0b', fontSize:10}}/>}
                </ComposedChart>
              </ResponsiveContainer>
            </div>
            <table style={{width:'100%',fontSize:11,borderCollapse:'collapse'}}>
              <thead>
                <tr style={{color:'#64748b',textAlign:'right'}}>
                  <th style={{textAlign:'left',padding:'4px 6px',fontWeight:500}}>Scenario</th>
                  <th style={{padding:'4px 6px',fontWeight:500}}>4-yr COA</th>
                  <th style={{padding:'4px 6px',fontWeight:500}}>Gap</th>
                  <th style={{padding:'4px 6px',fontWeight:500}}>Need $/mo</th>
                </tr>
              </thead>
              <tbody>
                {k.scenarios.map(s => {
                  const onTrack = s.gap <= 0;
                  return (
                    <tr key={s.key} style={{borderTop:'1px solid #1e293b'}}>
                      <td style={{padding:'5px 6px',color:'#cbd5e1'}}>{s.label}</td>
                      <td style={{padding:'5px 6px',textAlign:'right',color:'#cbd5e1'}}>{fmtMoney(s.cost)}</td>
                      <td style={{padding:'5px 6px',textAlign:'right',color:onTrack?'#10b981':'#f59e0b',fontWeight:600}}>
                        {onTrack ? '+' + fmtMoney(-s.gap) : '−' + fmtMoney(s.gap)}
                      </td>
                      <td style={{padding:'5px 6px',textAlign:'right',color:onTrack?'#64748b':'#e2e8f0',fontWeight:600}}>
                        {onTrack ? '0' : fmtMoneyExact(s.monthlyNeeded)}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        ))}
      </div>
    </div>
  );
}

// ── 529 loading + generational transfer ──────────────────────────────────────
// The card that answers the two questions CollegeTrajectoryCard doesn't:
// how hard to load the 529s NOW (superfund capacity + what front-loading is
// worth over dripping), and what the leftover does afterwards — renamed to a
// grandchild, bled into a Roth, or cashed out at a price. All arithmetic in
// lib/plan529; indexed IRS numbers (gift exclusion, IRA limit) come from
// TaxConstants so this card can never carry a stale copy of its own.
function GenerationalCard({ kids, kidAccounts, assignments, monthlyByAccount, cfg, setCfg, currentYear }) {
  const Plan529 = window.Plan529, TaxConstants = window.TaxConstants;
  if (!Plan529 || !TaxConstants) return null;
  const tax = TaxConstants.forYear(currentYear);
  const excl = tax?.gift?.annualExclusion || 0;
  const iraLimit = tax?.ira?.limit || 0;
  const byKid = cfg.byKid || {};
  const setKidCfg = (kidId, field, value) =>
    setCfg(c => ({ ...c, byKid: { ...(c.byKid || {}), [kidId]: { ...((c.byKid || {})[kidId] || {}), [field]: value } } }));

  // Only 529s can be renamed to a grandchild or rolled to a Roth — a UTMA is
  // the child's property already, so it seeds nothing here and we say so.
  const is529 = (a) => /529|education/i.test(
    `${a.subtype?.name || a.subtype?.display || ''} ${a.type?.name || ''} ${a.displayName || a.name || ''}`);

  const rows = kids.filter(k => k.name && k.dob).map(k => {
    const kc = byKid[k.id] || {};
    const birthYear = new Date(k.dob).getFullYear();
    const accts = kidAccounts.filter(a => assignments[a.id] === k.id);
    const accts529 = accts.filter(is529);
    const balance = accts529.reduce((s, a) => s + (a.currentBalance || 0), 0);
    const monthly = accts529.reduce((s, a) => s + (Number(monthlyByAccount[a.id]) || 0), 0);
    const nonTransferable = accts.length - accts529.length;
    const collegeStart = k.collegeStartYear ? Number(k.collegeStartYear) : birthYear + 18;
    const scenario = COLLEGE_SCENARIOS_2025.find(s => s.key === (kc.collegeKey || 'outOfStatePublicAvg')) || COLLEGE_SCENARIOS_2025[1];
    const tuition = Number(kc.annualTuition) || 0;
    const schoolFirst = Math.max(currentYear, birthYear + 5);
    const schoolLast = Math.min(collegeStart - 1, birthYear + 17);
    const plan = Plan529.buildGenerationalPlan({
      name: k.name, birthYear, balance,
      annualContribution: monthly * 12,
      frontLoad: Number(kc.frontLoad) || 0,
      accountOpenedYear: kc.accountOpenedYear ? Number(kc.accountOpenedYear) : null,
      k12: tuition > 0 && schoolLast >= schoolFirst
        ? { annualTuition: tuition, firstYear: schoolFirst, lastYear: schoolLast } : null,
      college: { startYear: collegeStart, years: 4, annualCost: scenario.annualCOA },
    }, {
      currentYear, returnPct: REAL_RETURN_PCT, eduInflationPct: EDU_INFLATION_PCT,
      annualExclusion: excl, donors: 2, iraLimit,
      marginalRatePct: 24, statePct: 6.6,
      grandchild: { ageAtFirstChild: Number(cfg.ageAtFirstChild) || 30 },
    });
    return { kid: k, kc, plan, balance, monthly, nonTransferable, tuition, schoolFirst, schoolLast, collegeStart, scenario };
  });

  if (rows.length === 0) return null;

  const structure = Plan529.compareAccountStructures(rows.map(r => ({
    name: r.kid.name,
    k12: r.tuition > 0 && r.schoolLast >= r.schoolFirst ? { firstYear: r.schoolFirst, lastYear: r.schoolLast } : null,
    college: { startYear: r.collegeStart, years: 4 },
  })), { annualExclusion: excl, donors: 2 });

  const totalFrontLoad = rows.reduce((s, r) => s + (Number(r.kc.frontLoad) || 0), 0);
  const label = { fontSize: 10, marginBottom: 2 };
  const box = { border: '1px solid #1e293b', borderRadius: 8, padding: '10px 12px', background: '#0b1220' };
  const exitBox = { flex: '1 1 180px', background: '#0f1520', border: '1px solid #1e2a3a', borderRadius: 8, padding: '10px 12px' };

  return (
    <div className="card" style={{marginBottom:16}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',flexWrap:'wrap',gap:8,marginBottom:4}}>
        <div style={{fontWeight:600,fontSize:14}}>529 Loading &amp; Generational Transfer</div>
        <div style={{fontSize:11,color:'#64748b'}}>
          gift exclusion {excl ? fmtMoneyExact(excl) : '—'}/donor
          {tax?.stale && <span style={{marginLeft:6,color:'#f59e0b'}}>({tax.year} figure — taxConstants needs {currentYear})</span>}
          {totalFrontLoad > 0 && <span style={{marginLeft:10}}>planned front-load <span style={{color:'#e2e8f0',fontWeight:600}}>{fmtMoneyExact(totalFrontLoad)}</span></span>}
        </div>
      </div>
      <div style={{fontSize:11,color:'#64748b',marginBottom:12}}>
        Superfund room, school + college paid from the plan (K-12 draws capped at the statutory {fmtMoneyExact(Plan529.k12CapForYear(currentYear))}/yr — the cap never inflates, tuition does),
        and the leftover's three exits. Assumes each kid's first child at age <input type="number" value={cfg.ageAtFirstChild ?? 30}
          onChange={e=>setCfg(c=>({...c, ageAtFirstChild: Number(e.target.value)||30}))}
          style={{width:44,fontSize:11,padding:'1px 4px',margin:'0 2px'}}/> with the same school + college pattern. Save with the Save button above.
      </div>
      <div style={{display:'flex',flexDirection:'column',gap:14}}>
        {rows.map(({ kid: k, kc, plan, balance, nonTransferable, tuition, schoolFirst, schoolLast, collegeStart, scenario }) => {
          const firstSchoolRow = plan.projection.path.find(p => p.k12Billed > 0);
          const t = plan.projection.totals;
          return (
            <div key={k.id} style={box}>
              <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',flexWrap:'wrap',gap:6,marginBottom:8}}>
                <div style={{fontWeight:600,fontSize:13}}>{k.name}</div>
                <div style={{fontSize:11,color:'#64748b'}}>
                  529 balance <span style={{color:'#10b981',fontWeight:600}}>{fmtMoneyExact(balance)}</span>
                  {nonTransferable > 0 && <span style={{color:'#f59e0b'}}> · {nonTransferable} UTMA/custodial account{nonTransferable>1?'s':''} excluded — already the child's property, can't be renamed</span>}
                </div>
              </div>

              <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(150px,1fr))',gap:10,marginBottom:10}}>
                <div>
                  <div className="label" style={label}>Private school $/yr (today's)</div>
                  <input type="number" value={kc.annualTuition||''} onChange={e=>setKidCfg(k.id,'annualTuition',e.target.value)} placeholder="e.g. 28000"/>
                </div>
                <div>
                  <div className="label" style={label}>Front-load now ($)</div>
                  <input type="number" value={kc.frontLoad||''} onChange={e=>setKidCfg(k.id,'frontLoad',e.target.value)} placeholder="0"/>
                </div>
                <div>
                  <div className="label" style={label}>529 opened (year)</div>
                  <input type="number" value={kc.accountOpenedYear||''} onChange={e=>setKidCfg(k.id,'accountOpenedYear',e.target.value)} placeholder="for Roth 15-yr rule"/>
                </div>
                <div>
                  <div className="label" style={label}>College scenario</div>
                  <select value={kc.collegeKey||'outOfStatePublicAvg'} onChange={e=>setKidCfg(k.id,'collegeKey',e.target.value)}>
                    {COLLEGE_SCENARIOS_2025.map(s=><option key={s.key} value={s.key}>{s.label}</option>)}
                  </select>
                </div>
              </div>

              <div style={{fontSize:11,color:'#94a3b8',display:'flex',flexDirection:'column',gap:4,marginBottom:10}}>
                {!plan.loading.unknown && (
                  <div>
                    <span style={{color:'#64748b'}}>Load it now:</span> room for <strong style={{color:'#e2e8f0'}}>{fmtMoneyExact(plan.loading.perBeneficiary)}</strong> per
                    kid today (2 donors × 5 × {fmtMoneyExact(excl)}, the 5-year election).
                    {plan.frontLoad && plan.loading.withinCapacity && <> Your {fmtMoneyExact(plan.loading.plannedLump)} fits, and beats dripping it over 5 years by <strong style={{color:'#10b981'}}>{fmtMoney(plan.frontLoad.advantage)}</strong> by {currentYear + plan.frontLoad.horizonYears}.</>}
                    {plan.frontLoad && !plan.loading.withinCapacity && <span style={{color:'#f59e0b'}}> {fmtMoneyExact(plan.loading.excess)} of it exceeds the election — that slice files against the lifetime exemption.</span>}
                  </div>
                )}
                {firstSchoolRow && (
                  <div>
                    <span style={{color:'#64748b'}}>School ({schoolFirst}–{schoolLast}):</span> this year the 529 pays <strong style={{color:'#e2e8f0'}}>{fmtMoneyExact(firstSchoolRow.k12Draw)}</strong> of
                    the {fmtMoneyExact(firstSchoolRow.k12Billed)} bill{firstSchoolRow.k12FromCashflow > 0.5 && <>; <strong style={{color:'#f59e0b'}}>{fmtMoneyExact(firstSchoolRow.k12FromCashflow)}</strong> stays on cashflow</>}.
                    All school years: {fmtMoney(t.k12Draw)} from the plan, {fmtMoney(t.k12FromCashflow)} from cashflow.
                  </div>
                )}
                {tuition > 0 && !firstSchoolRow && <div style={{color:'#64748b'}}>School years already behind {k.name} — tuition input ignored.</div>}
                <div>
                  <span style={{color:'#64748b'}}>College ({collegeStart}–{collegeStart+3}, {scenario.label}):</span>{' '}
                  {t.collegeShortfall < 1
                    ? <>fully covered — <strong style={{color:'#10b981'}}>{fmtMoney(t.collegeDraw)}</strong> paid from the plan.</>
                    : <>{fmtMoney(t.collegeDraw)} of {fmtMoney(t.collegeBilled)} covered — <strong style={{color:'#f59e0b'}}>{fmtMoney(t.collegeShortfall)}</strong> short.</>}
                </div>
              </div>

              {plan.leftover > 0.5 ? (
                <div>
                  <div style={{fontSize:11,color:'#64748b',marginBottom:6}}>
                    Leftover after college: <strong style={{color:'#10b981',fontSize:13}}>{fmtMoney(plan.leftover)}</strong> in {plan.leftoverYear} — three exits:
                  </div>
                  <div style={{display:'flex',flexWrap:'wrap',gap:8}}>
                    {plan.grandchild && (
                      <div style={exitBox} title={plan.grandchild.gstNote}>
                        <div style={{fontSize:11,fontWeight:600,color:'#3b82f6',marginBottom:4}}>Rename to a grandchild</div>
                        <div style={{fontSize:11,color:'#94a3b8'}}>
                          Covers <strong style={{color:'#e2e8f0'}}>{Math.round(plan.grandchild.coveragePct)}%</strong> of their school + college
                          (b. ~{plan.grandchild.grandkidBirthYear}, school {plan.grandchild.schoolFirstYear}, college {plan.grandchild.collegeStartYear}).
                          {plan.grandchild.leftoverAfter > 0.5 && <> Still {fmtMoney(plan.grandchild.leftoverAfter)} left after — generation three.</>}
                          <div style={{color:'#f59e0b',marginTop:3}}>Skips a generation → GST question for the lawyer first.</div>
                        </div>
                      </div>
                    )}
                    {plan.roth && !plan.roth.unknown && (
                      <div style={exitBox}>
                        <div style={{fontSize:11,fontWeight:600,color:'#10b981',marginBottom:4}}>529 → Roth IRA</div>
                        <div style={{fontSize:11,color:'#94a3b8'}}>
                          <strong style={{color:'#e2e8f0'}}>{fmtMoneyExact(plan.roth.totalRolled)}</strong> over {plan.roth.yearsToExhaustCap} yrs
                          from {plan.roth.firstYear} ({fmtMoneyExact(plan.roth.lifetimeCap)} lifetime cap, IRA-limit pace).
                          {plan.roth.balanceAfter > 0.5 && <> Leaves {fmtMoney(plan.roth.balanceAfter)} needing another exit.</>}
                          <div style={{color:'#64748b',marginTop:3}}>Needs earned income; uses their own IRA room.</div>
                        </div>
                      </div>
                    )}
                    {plan.nonQualified && (
                      <div style={exitBox}>
                        <div style={{fontSize:11,fontWeight:600,color:'#f59e0b',marginBottom:4}}>Cash out (non-qualified)</div>
                        <div style={{fontSize:11,color:'#94a3b8'}}>
                          Nets <strong style={{color:'#e2e8f0'}}>{fmtMoney(plan.nonQualified.net)}</strong> of {fmtMoney(plan.nonQualified.amount)} —
                          tax + 10% penalty on earnings only ({Math.round(plan.nonQualified.effectiveCostPct)}% haircut at 24% + 6.6% DE).
                        </div>
                      </div>
                    )}
                  </div>
                </div>
              ) : (
                <div style={{fontSize:11,color:'#64748b'}}>
                  Nothing left over — the plan spends out{t.collegeShortfall >= 1 && <> and still comes up {fmtMoney(t.collegeShortfall)} short of college</>}. Front-loading is what changes this number.
                </div>
              )}
            </div>
          );
        })}
      </div>

      {/* ── One pot or one per kid ── */}
      <div style={{...box, marginTop:14}}>
        <div style={{fontWeight:600,fontSize:13,marginBottom:2}}>One pot, or one account per kid?</div>
        <div style={{fontSize:11,color:'#64748b',marginBottom:10}}>
          Three lines of the statute are per <em>beneficiary</em>, so the structure changes what the same dollars can do.
          Ownership doesn't: you control every account either way.
        </div>
        <table style={{width:'100%',fontSize:11,borderCollapse:'collapse',marginBottom:10}}>
          <thead>
            <tr style={{color:'#64748b',textAlign:'right'}}>
              <th style={{textAlign:'left',padding:'4px 6px',fontWeight:500}}></th>
              <th style={{padding:'4px 6px',fontWeight:500}}>Per kid ({structure.children})</th>
              <th style={{padding:'4px 6px',fontWeight:500}}>One pot</th>
            </tr>
          </thead>
          <tbody>
            <tr style={{borderTop:'1px solid #1e293b'}}>
              <td style={{padding:'5px 6px',color:'#cbd5e1'}}>Front-load room today (5-yr election)</td>
              <td style={{padding:'5px 6px',textAlign:'right',color:'#10b981',fontWeight:600}}>{structure.perChild.superfundCapacity != null ? fmtMoneyExact(structure.perChild.superfundCapacity) : '—'}</td>
              <td style={{padding:'5px 6px',textAlign:'right',color:'#cbd5e1'}}>{structure.pooled.superfundCapacity != null ? fmtMoneyExact(structure.pooled.superfundCapacity) : '—'}</td>
            </tr>
            <tr style={{borderTop:'1px solid #1e293b'}}>
              <td style={{padding:'5px 6px',color:'#cbd5e1'}}>K-12 caps in the {structure.pooled.overlapYears.length || 'no'} year{structure.pooled.overlapYears.length===1?'':'s'} both are in school/college</td>
              <td style={{padding:'5px 6px',textAlign:'right',color:'#10b981',fontWeight:600}}>{structure.pooled.overlapYears.length ? `${structure.children} × ${fmtMoneyExact(Plan529.k12CapForYear(currentYear))}/yr` : '—'}</td>
              <td style={{padding:'5px 6px',textAlign:'right',color:structure.pooled.overlapYears.length?'#f59e0b':'#cbd5e1'}}>{structure.pooled.overlapYears.length ? `1 × ${fmtMoneyExact(Plan529.k12CapForYear(currentYear))}/yr` : '—'}</td>
            </tr>
            <tr style={{borderTop:'1px solid #1e293b'}}>
              <td style={{padding:'5px 6px',color:'#cbd5e1'}}>529 → Roth lifetime valves</td>
              <td style={{padding:'5px 6px',textAlign:'right',color:'#10b981',fontWeight:600}}>{fmtMoneyExact(structure.perChild.rothCapTotal)}</td>
              <td style={{padding:'5px 6px',textAlign:'right',color:'#cbd5e1'}}>{fmtMoneyExact(structure.pooled.rothCapTotal)}</td>
            </tr>
          </tbody>
        </table>
        <div style={{fontSize:11,color:'#94a3b8',display:'flex',flexDirection:'column',gap:4}}>
          {structure.notes.map((n, i) => <div key={i}>· {n}</div>)}
        </div>
      </div>
    </div>
  );
}

// ── Kids Financial Planning ──────────────────────────────────────────────────
// Kid profiles + auto-detect 529/UTMA/custodial accounts from Monarch + AI
// advice card that knows what's already in place. Stored at
// users/{uid}/kidsPlan/config.
function Kids() {
  const { kidsPlan, saveKidsPlan, allAccounts, retirementConfig, compensation, cashflow, holdings, investmentTargets } = useContext(DataContext);
  const { show, Toast } = useToast();
  const currentYear = new Date().getFullYear();

  const [kids, setKids] = useState([]);
  const [assignments, setAssignments] = useState({});
  const [monthlyByAccount, setMonthlyByAccount] = useState({});
  const [kidPosture, setKidPosture] = useState({});       // { kidId: { hasEarnedIncome, specialNeeds, inDaycare } }
  const [plan529Cfg, setPlan529Cfg] = useState({ byKid: {}, ageAtFirstChild: 30 });
  const [householdPosture, setHouseholdPosture] = useState({
    termLifeYouCoverage: '', termLifeSpouseCoverage: '',
    willInPlace: false, guardianshipDesignated: false,
    powerOfAttorney: false, livingTrust: false,
    emergencyFundMonths: '', notes: ''
  });
  const [saving, setSaving] = useState(false);
  const [insights, setInsights] = useState('');
  const [loadingAi, setLoadingAi] = useState(false);

  const [insightsAt, setInsightsAt] = useState(null);

  useEffect(() => {
    if (!kidsPlan) return;
    setKids(Array.isArray(kidsPlan.kids) ? kidsPlan.kids : []);
    setAssignments(kidsPlan.accountAssignments || {});
    setMonthlyByAccount(kidsPlan.monthlyContributions || {});
    setKidPosture(kidsPlan.kidPosture || {});
    if (kidsPlan.plan529) setPlan529Cfg(p => ({ ...p, ...kidsPlan.plan529 }));
    setHouseholdPosture(p => ({ ...p, ...(kidsPlan.householdPosture || {}) }));
    if (kidsPlan.insights) setInsights(kidsPlan.insights);
    if (kidsPlan.insightsGeneratedAt) setInsightsAt(kidsPlan.insightsGeneratedAt);
  }, [kidsPlan]);

  // Detect kid-linked accounts from Monarch: subtype/name heuristic catches
  // 529, UTMA, custodial, and education-savings variants. The user can still
  // assign any account to any kid via the dropdown.
  const kidAccounts = useMemo(() => {
    const matches = (a) => {
      const st = (a.subtype?.name || a.subtype?.display || '').toLowerCase();
      const tp = (a.type?.name || a.type?.display || '').toLowerCase();
      const nm = (a.displayName || a.name || '').toLowerCase();
      return /529|utma|ugma|custodial|education/.test(`${st} ${tp} ${nm}`);
    };
    return (allAccounts || []).filter(a => a.isAsset !== false).filter(matches);
  }, [allAccounts]);

  const kidTotal = (kid) => kidAccounts
    .filter(a => assignments[a.id] === kid.id)
    .reduce((s, a) => s + (a.currentBalance || 0), 0);

  const addKid = () => {
    setKids(k => [...k, { id: `kid_${Date.now()}`, name: '', dob: '', collegeStartYear: '' }]);
  };
  const updateKid = (id, field, value) => {
    setKids(k => k.map(x => x.id === id ? { ...x, [field]: value } : x));
  };
  const removeKid = (id) => {
    setKids(k => k.filter(x => x.id !== id));
    setAssignments(a => {
      const next = { ...a };
      Object.keys(next).forEach(k => { if (next[k] === id) delete next[k]; });
      return next;
    });
  };

  const handleSave = async () => {
    setSaving(true);
    try {
      await saveKidsPlan({
        kids: kids.filter(k => k.name),
        accountAssignments: assignments,
        monthlyContributions: monthlyByAccount,
        kidPosture,
        plan529: plan529Cfg,
        householdPosture,
        // Don't drop a previously-generated AI plan when saving config edits.
        ...(insights ? { insights, insightsGeneratedAt: insightsAt || Date.now() } : {}),
      });
      show('Kids plan saved');
    } catch (e) { show(e.message, 'error'); }
    setSaving(false);
  };

  const handleGenerateAi = async () => {
    setLoadingAi(true); setInsights('');
    try {
      const kidsWithAges = kids.filter(k => k.name && k.dob).map(k => {
        const age = calcAge(k.dob);
        const yearsToCollege = k.collegeStartYear ? (Number(k.collegeStartYear) - currentYear) : (18 - age);
        const accts = kidAccounts.filter(a => assignments[a.id] === k.id).map(a => ({
          id: a.id,
          name: a.displayName || a.name,
          type: a.subtype?.display || a.type?.display || 'account',
          balance: Math.round(a.currentBalance || 0),
          monthlyContribution: monthlyByAccount[a.id] || 0,
        }));
        return {
          name: k.name,
          age,
          yearsToCollege: Math.max(0, yearsToCollege),
          posture: kidPosture[k.id] || {},
          accounts: accts,
          totalBalance: accts.reduce((s, a) => s + a.balance, 0),
          totalMonthlyContribution: accts.reduce((s, a) => s + (a.monthlyContribution || 0), 0),
        };
      });

      // Cashflow is the ground truth for net take-home: actual money hitting
      // bank accounts after taxes and pre-tax deductions (incl retirement),
      // averaged over the last 6 months. This — not gross-comp annualization
      // — drives capacity. RSU vests/sales already show up here, so we do
      // NOT also pass compensation.history (which counts RSU at FMV and
      // would double-count). Avg monthly net = (sumIncome over 6 months) / 6.
      const cashflowByMonth = cashflow?.byMonth || [];
      const last6 = cashflowByMonth.slice(-6);
      const monthlyNetTakeHomeFromCashflow = last6.length
        ? Math.round(last6.reduce((s, m) => s + (m.sumIncome || 0), 0) / last6.length)
        : null;
      const monthlyExpenseFromCashflow = last6.length
        ? Math.round(last6.reduce((s, m) => s + Math.abs(m.sumExpense || 0), 0) / last6.length)
        : null;
      const monthlyNetSavingsFromCashflow = (monthlyNetTakeHomeFromCashflow != null && monthlyExpenseFromCashflow != null)
        ? monthlyNetTakeHomeFromCashflow - monthlyExpenseFromCashflow
        : null;

      const totalKidMonthly = Object.values(monthlyByAccount).reduce((s, v) => s + (Number(v) || 0), 0);

      const r = await callFn('generateKidsPlan', {
        kids: kidsWithAges,
        household: {
          filingStatus: 'MFJ',
          state: 'Delaware',
          // Cashflow truth: 6 months of bank-deposit/expense activity.
          // Already net of taxes and pre-tax deductions (incl retirement).
          monthlyNetTakeHome: monthlyNetTakeHomeFromCashflow,
          monthlyExpense: monthlyExpenseFromCashflow,
          monthlyNetSavings: monthlyNetSavingsFromCashflow,
          currentKidMonthlyContribution: totalKidMonthly,
          posture: householdPosture,
        },
        cashflowRecent: last6,
        currentVehicles: ['529 plans', 'UTMA'],
      });
      setInsights(r.insights);
      const now = Date.now();
      setInsightsAt(now);
      // Persist alongside the rest of the kids plan so the generated plan
      // survives navigation, refresh, and re-login.
      try {
        await saveKidsPlan({
          kids: kids.filter(k => k.name),
          accountAssignments: assignments,
          monthlyContributions: monthlyByAccount,
          kidPosture,
          plan529: plan529Cfg,
          householdPosture,
          insights: r.insights,
          insightsGeneratedAt: now,
        });
      } catch (saveErr) {
        // Non-fatal: insights are still rendered from state; just couldn't cache.
        console.warn('Failed to persist generated kids plan:', saveErr);
      }
    } catch (e) { show(e.message, 'error'); }
    setLoadingAi(false);
  };

  return (
    <div className="page">
      {Toast}
      <div className="page-title">Kids</div>

      {/* ── Profile ── */}
      <div className="card" style={{marginBottom:16}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
          <div>
            <div style={{fontWeight:600,fontSize:14}}>Kids Profiles</div>
            <div style={{fontSize:11,color:'#64748b',marginTop:2}}>Name + DOB drives age, years to college, and account assignments</div>
          </div>
          <button className="btn-secondary" onClick={addKid} style={{padding:'6px 12px',fontSize:12}}>+ Add Kid</button>
        </div>
        {kids.length === 0 ? (
          <div style={{padding:'20px',textAlign:'center',color:'#475569',fontSize:12}}>No kids added yet — click "Add Kid" to get started.</div>
        ) : (
          <div style={{display:'flex',flexDirection:'column',gap:12}}>
            {kids.map(k => {
              const age = k.dob ? calcAge(k.dob) : null;
              const collegeIn = k.dob ? Math.max(0, 18 - age) : null;
              return (
                <div key={k.id} style={{background:'#0f1520',border:'1px solid #1e2a3a',borderRadius:8,padding:12}}>
                  <div style={{display:'grid',gridTemplateColumns:'2fr 1.4fr 1.2fr auto',gap:10,alignItems:'end'}}>
                    <div>
                      <div className="label" style={{fontSize:11,marginBottom:3}}>Name</div>
                      <input value={k.name||''} onChange={e=>updateKid(k.id,'name',e.target.value)} placeholder="Kid's name"/>
                    </div>
                    <div>
                      <div className="label" style={{fontSize:11,marginBottom:3}}>Date of Birth</div>
                      <input type="date" value={k.dob||''} onChange={e=>updateKid(k.id,'dob',e.target.value)}/>
                    </div>
                    <div>
                      <div className="label" style={{fontSize:11,marginBottom:3}}>College Start (year)</div>
                      <input type="number" min="2020" max="2050" value={k.collegeStartYear||''} onChange={e=>updateKid(k.id,'collegeStartYear',e.target.value)} placeholder={age!=null?String(currentYear + collegeIn):''}/>
                    </div>
                    <button className="btn-secondary" onClick={()=>removeKid(k.id)} style={{padding:'6px 10px',fontSize:11,color:'#ef4444'}}>Remove</button>
                  </div>
                  {age!=null && (
                    <div style={{marginTop:8,fontSize:11,color:'#64748b'}}>
                      Age <strong style={{color:'#e2e8f0'}}>{age}</strong> · {collegeIn>0 ? <>college in <strong style={{color:'#f59e0b'}}>{collegeIn} yrs</strong></> : 'college age'} · current savings <strong style={{color:'#10b981'}}>{fmtCur(kidTotal(k))}</strong>
                    </div>
                  )}
                  <div style={{marginTop:10,display:'flex',flexWrap:'wrap',gap:8}}>
                    {[
                      {k:'hasEarnedIncome',label:'Has W-2 earned income'},
                      {k:'inDaycare',label:'In daycare/preschool'},
                      {k:'specialNeeds',label:'Special needs'},
                    ].map(opt=>{
                      const on = kidPosture[k.id]?.[opt.k];
                      return (
                        <label key={opt.k} style={{display:'flex',alignItems:'center',gap:6,fontSize:11,padding:'4px 9px',background:on?'rgba(59,130,246,0.15)':'#161b22',border:`1px solid ${on?'rgba(59,130,246,0.4)':'#1e2a3a'}`,borderRadius:6,cursor:'pointer',color:on?'#3b82f6':'#94a3b8'}}>
                          <input type="checkbox" checked={!!on} onChange={e=>setKidPosture(s=>({...s,[k.id]:{...(s[k.id]||{}),[opt.k]:e.target.checked}}))} style={{margin:0}}/>
                          {opt.label}
                        </label>
                      );
                    })}
                  </div>
                </div>
              );
            })}
          </div>
        )}
      </div>

      {/* ── Account assignment ── */}
      <div className="card" style={{marginBottom:16}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
          <div>
            <div style={{fontWeight:600,fontSize:14}}>Detected 529 / UTMA / Custodial Accounts</div>
            <div style={{fontSize:11,color:'#64748b',marginTop:2}}>Auto-matched from Monarch by subtype + name. Assign each to a kid.</div>
          </div>
          <button className="btn-primary" onClick={handleSave} disabled={saving} style={{padding:'6px 14px',fontSize:12}}>{saving?'Saving…':'Save'}</button>
        </div>
        {kidAccounts.length === 0 ? (
          <div style={{padding:'20px',textAlign:'center',color:'#475569',fontSize:12}}>
            No 529/UTMA/custodial accounts detected. Re-sync Monarch or check account types.
          </div>
        ) : (
          <div style={{display:'flex',flexDirection:'column',gap:8}}>
            {kidAccounts.map(a => (
              <div key={a.id} style={{display:'grid',gridTemplateColumns:'2fr 1fr 1.2fr 1.2fr',gap:10,alignItems:'center',padding:'10px 12px',background:'#0f1520',border:'1px solid #1e2a3a',borderRadius:8}}>
                <div>
                  <div style={{fontSize:13,fontWeight:600,color:'#e2e8f0'}}>{a.displayName || a.name}</div>
                  <div style={{fontSize:10,color:'#64748b'}}>{a.subtype?.display || a.type?.display || '—'}</div>
                </div>
                <div style={{fontSize:13,fontWeight:700,color:'#10b981'}}>{fmtCur(a.currentBalance || 0)}</div>
                <div>
                  <div className="label" style={{fontSize:10,marginBottom:2}}>Assigned to</div>
                  <select value={assignments[a.id]||''} onChange={e=>setAssignments(s=>({...s,[a.id]:e.target.value}))}>
                    <option value="">— unassigned —</option>
                    {kids.filter(k=>k.name).map(k=><option key={k.id} value={k.id}>{k.name}</option>)}
                  </select>
                </div>
                <div>
                  <div className="label" style={{fontSize:10,marginBottom:2}}>Monthly $</div>
                  <input type="number" value={monthlyByAccount[a.id]||''} onChange={e=>setMonthlyByAccount(s=>({...s,[a.id]:Number(e.target.value)||0}))} placeholder="0"/>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>

      {/* ── College trajectory (deterministic, no AI) ── */}
      <CollegeTrajectoryCard
        kids={kids}
        kidAccounts={kidAccounts}
        assignments={assignments}
        monthlyByAccount={monthlyByAccount}
        currentYear={currentYear}
      />

      {/* ── 529 loading + generational transfer (deterministic, lib/plan529) ── */}
      <GenerationalCard
        kids={kids}
        kidAccounts={kidAccounts}
        assignments={assignments}
        monthlyByAccount={monthlyByAccount}
        cfg={plan529Cfg}
        setCfg={setPlan529Cfg}
        currentYear={currentYear}
      />

      {/* ── Household posture ── */}
      <div className="card" style={{marginBottom:16}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
          <div>
            <div style={{fontWeight:600,fontSize:14}}>What's Already in Place</div>
            <div style={{fontSize:11,color:'#64748b',marginTop:2}}>The AI will build on these, not re-suggest them</div>
          </div>
        </div>
        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(200px,1fr))',gap:10,marginBottom:12}}>
          <div>
            <div className="label" style={{fontSize:11,marginBottom:3}}>Term life — you ($ coverage)</div>
            <input type="number" value={householdPosture.termLifeYouCoverage||''} onChange={e=>setHouseholdPosture(s=>({...s,termLifeYouCoverage:e.target.value}))} placeholder="e.g. 1500000"/>
          </div>
          <div>
            <div className="label" style={{fontSize:11,marginBottom:3}}>Term life — spouse ($ coverage)</div>
            <input type="number" value={householdPosture.termLifeSpouseCoverage||''} onChange={e=>setHouseholdPosture(s=>({...s,termLifeSpouseCoverage:e.target.value}))} placeholder="e.g. 750000"/>
          </div>
          <div>
            <div className="label" style={{fontSize:11,marginBottom:3}}>Emergency fund (months)</div>
            <input type="number" value={householdPosture.emergencyFundMonths||''} onChange={e=>setHouseholdPosture(s=>({...s,emergencyFundMonths:e.target.value}))} placeholder="e.g. 6"/>
          </div>
        </div>
        <div style={{display:'flex',flexWrap:'wrap',gap:8,marginBottom:12}}>
          {[
            {k:'willInPlace',label:'Wills in place'},
            {k:'guardianshipDesignated',label:'Guardianship designated'},
            {k:'powerOfAttorney',label:'Durable POA + healthcare proxy'},
            {k:'livingTrust',label:'Living/revocable trust'},
          ].map(opt=>{
            const on = householdPosture[opt.k];
            return (
              <label key={opt.k} style={{display:'flex',alignItems:'center',gap:6,fontSize:11,padding:'5px 10px',background:on?'rgba(59,130,246,0.15)':'#161b22',border:`1px solid ${on?'rgba(59,130,246,0.4)':'#1e2a3a'}`,borderRadius:6,cursor:'pointer',color:on?'#3b82f6':'#94a3b8'}}>
                <input type="checkbox" checked={!!on} onChange={e=>setHouseholdPosture(s=>({...s,[opt.k]:e.target.checked}))} style={{margin:0}}/>
                {opt.label}
              </label>
            );
          })}
        </div>
        <div>
          <div className="label" style={{fontSize:11,marginBottom:3}}>Other context (daycare cost, inheritance expected, specific goals — optional)</div>
          <textarea value={householdPosture.notes||''} onChange={e=>setHouseholdPosture(s=>({...s,notes:e.target.value}))} rows={2} style={{fontFamily:'inherit',fontSize:12,resize:'vertical',width:'100%'}} placeholder="e.g. $32k/yr dependent care, wife's pension covers ~40% of retirement income, already maxing HSA"/>
        </div>
      </div>

      {/* ── AI planning card ── */}
      <div className="card">
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',flexWrap:'wrap',gap:10,marginBottom:14}}>
          <div>
            <div style={{fontWeight:600,fontSize:14}}>AI Kids Plan</div>
            <div style={{fontSize:11,color:'#64748b',marginTop:2}}>Personalized advice given your income, retirement plan, and current kid accounts — won't re-recommend 529/UTMA since you're already using them</div>
          </div>
          <div style={{display:'flex',alignItems:'center',gap:10}}>
            {insights && insightsAt && !loadingAi && (
              <span style={{fontSize:11,color:'#64748b'}}>
                cached {(() => {
                  const ms = Date.now() - insightsAt;
                  const m = Math.floor(ms / 60000);
                  if (m < 1) return 'just now';
                  if (m < 60) return `${m}m ago`;
                  const h = Math.floor(m / 60);
                  if (h < 24) return `${h}h ago`;
                  return `${Math.floor(h / 24)}d ago`;
                })()}
              </span>
            )}
            <button className="btn-primary" onClick={handleGenerateAi} disabled={loadingAi||kids.length===0} style={{padding:'8px 16px'}}>
              {loadingAi?<span className="spinner"/>:(insights?'Regenerate':'Generate Plan')}
            </button>
          </div>
        </div>
        {insights ? (
          <MarkdownView text={insights} />
        ) : (
          <div style={{padding:'24px 20px',textAlign:'center',color:'#475569',fontSize:12}}>
            {kids.length===0
              ? 'Add at least one kid above, then click Generate Plan.'
              : 'Click "Generate Plan" for dollar-quantified next moves: monthly contribution split, custodial Roth timing, when a living trust becomes worthwhile, and how to balance with your retirement glidepath.'}
          </div>
        )}
      </div>
    </div>
  );
}

window.FinanceViews = Object.assign(window.FinanceViews || {}, { Kids });
