// Himma demo data — Layla Haddad, Dubai, AED 780K net worth

const USER = {
  name: 'Layla Haddad',
  firstName: 'Layla',
  email: 'layla.haddad@meraki.media',
  phone: '+971 50 4218 ••••',
  location: 'Dubai, UAE',
  member: 'since Feb 2025',
  emiratesId: '784-1993-•••••••-•',
  // Cohort dimensions (drive peer comparison)
  age: 31,
  ageBracket: '25-35',
  gender: 'female',
  city: 'dubai',
  incomeTier: '15-25k',
};

// Notifications / inbox
const NOTIFICATIONS = [
  { id:'n1',  group:'Today',     kind:'insight',  unread:true,  icon:'sparkles', title:'Morning note from Himma',
    body:'Portfolio up 4.2% this month — Saxo world-index drove AED 18.4K of gains.', when:'09:02', accent:true },
  { id:'n2',  group:'Today',     kind:'alert',    unread:true,  icon:'bolt',     title:'BTC moved +2.1% in 24h',
    body:'Your Bitcoin position is now AED 61,020 (up AED 1,254 today).', when:'08:48' },
  { id:'n3',  group:'Today',     kind:'tx',       unread:true,  icon:'download', title:'Salary received — AED 24,800',
    body:'Meraki Media · FAB Salary •• 0337', when:'09:02' },
  { id:'n4',  group:'Today',     kind:'tx',       unread:false, icon:'cart',     title:'Spinneys Dubai Marina',
    body:'AED 342.50 on Emirates NBD •• 4821', when:'08:14' },
  { id:'n5',  group:'Yesterday', kind:'rec',      unread:false, icon:'star',     title:'New recommendation',
    body:'AED 45K idle in Emirates NBD Current — high-yield options available.', when:'17:30', accent:true },
  { id:'n6',  group:'Yesterday', kind:'system',   unread:false, icon:'shield',   title:'New device signed in',
    body:'iPhone 15 Pro · Dubai · approved by you', when:'14:12' },
  { id:'n7',  group:'Yesterday', kind:'tx',       unread:false, icon:'chart',    title:'Saxo — VWRL purchase filled',
    body:'12 shares at AED 266.67 · AED 3,200 total', when:'17:22' },
  { id:'n8',  group:'This week', kind:'billing',  unread:false, icon:'doc',      title:'Pro subscription renewed',
    body:'AED 468 charged to Visa •• 4821 · valid until 14 Feb 2027', when:'14 Feb' },
  { id:'n9',  group:'This week', kind:'alert',    unread:false, icon:'bell',     title:'DEWA bill posted',
    body:'AED 612.00 due 22 Apr — pay from ADCB Savings?', when:'12 Apr' },
  { id:'n10', group:'This week', kind:'system',   unread:false, icon:'refresh',  title:'Wise account synced',
    body:'GBP 2,140 · USD 3,820 imported successfully.', when:'11 Apr' },
];

// Active subscription
const SUBSCRIPTION = {
  plan: 'Himma Pro',
  status: 'active',
  pricePerMonth: 39,                // annual billing — effective monthly rate
  monthlyPrice: 49,                 // standalone monthly plan rate
  cycle: 'annual',                  // billed yearly
  nextBilling: '14 Feb 2027',
  renewsAmount: 468,                // AED 39 × 12
  startedOn: '14 Feb 2026',
  trialEnded: '28 Feb 2026',
  paymentMethod: { brand:'Visa', last4:'4821', expiry:'08/28' },
  benefits: [
    'All asset classes — crypto, brokerages, real estate',
    'Unlimited AI chat & analysis',
    'Personalised recommendations engine',
    'Priority human support',
    'Export to CSV + accountant sharing',
  ],
  invoices: [
    { id:'inv-2026-02', date:'14 Feb 2026', amount: 468, status:'paid', period:'14 Feb 2026 — 14 Feb 2027' },
    { id:'inv-2026-01', date:'28 Jan 2026', amount:   0, status:'trial', period:'14-day trial' },
  ],
};

const PLAN_BENEFITS_PRO = [
  { icon: 'chart',    text: "See how you're tracking vs. peers" },
  { icon: 'sparkles', text: 'Get an AI advisor in your pocket' },
  { icon: 'coin',     text: 'Spot savings — Pro users have saved AED 1,890 on average' },
  { icon: 'shield',   text: 'Priority human support' },
  { icon: 'wallet',   text: 'Link unlimited accounts' },
];

const ANNUAL_VALUE_RECAP = [
  { value: 'AED 1,890', label: 'Saved via Insights' },
  { value: '245',       label: 'AI conversations' },
  { value: '12',        label: 'Accounts in sync' },
  { value: '4',         label: 'Priority support chats' },
];

const HELP_FAQS = [
  { id: 'link',   q: 'How do I link a new bank account?',
    a: "Go to Settings → Linked accounts → '+ Link a new account', then choose your institution and follow the secure connection flow." },
  { id: 'sec',    q: 'Is my financial data secure?',
    a: 'Yes — we use bank-grade encryption end-to-end. We never store your banking credentials and you can revoke access at any time.' },
  { id: 'calc',   q: 'How are balances calculated?',
    a: 'Balances are pulled live from each linked institution. Your net worth is the sum across banking, investments, crypto and real estate, minus liabilities.' },
  { id: 'sync',   q: "Why isn't my account syncing?",
    a: 'Most sync issues resolve within 24 hours. If it persists, try unlinking and relinking the account, or reach out via Settings → Contact us.' },
  { id: 'cancel', q: 'How do I cancel my Pro subscription?',
    a: "Settings → Subscription → Cancel subscription. You'll keep Pro features until the end of your current billing period." },
  { id: 'export', q: 'Can I export my data?',
    a: 'Yes — Settings → Export data lets you download balances and transactions as CSV or PDF for any date range.' },
];

// Asset breakdown — real estate is now MARKET VALUE (mortgage lives in LIABILITIES).
const ASSET_BREAKDOWN = [
  { key: 'banking',     label: 'Banking',       value: 186_240, color: '#D4B27A' },
  { key: 'investments', label: 'Investments',   value: 312_480, color: '#E8D4A8' },
  { key: 'crypto',      label: 'Crypto',        value:  94_300, color: '#9BB08A' },
  { key: 'realestate',  label: 'Real Estate',   value: 620_000, color: '#7389A8' },
  { key: 'other',       label: 'Other',         value:  28_400, color: '#C97B63' },
];

const ASSETS_TOTAL = ASSET_BREAKDOWN.reduce((s, x) => s + x.value, 0); // 1,241,420
const ASSETS_LAST_MONTH = 1_209_830;

// Liability breakdown (red palette — mortgages share the real-estate slate to cue the link)
const LIABILITY_BREAKDOWN = [
  { key: 'mortgages',    label: 'Mortgages',    value: 460_000, color: '#7389A8' },
  { key: 'loans',        label: 'Loans',        value:  75_600, color: '#C77A00' },
  { key: 'credit_cards', label: 'Credit Cards', value:   7_280, color: '#D23B2E' },
];

const LIABILITIES_TOTAL = LIABILITY_BREAKDOWN.reduce((s, x) => s + x.value, 0); // 542,880
const LIABILITIES_LAST_MONTH = 545_800;
const LIABILITIES_MOM = LIABILITIES_TOTAL - LIABILITIES_LAST_MONTH; // -2,920 (paid down)

const NET_WORTH = ASSETS_TOTAL - LIABILITIES_TOTAL; // 698,540
const NET_WORTH_LAST_MONTH = ASSETS_LAST_MONTH - LIABILITIES_LAST_MONTH; // 664,030
const NET_WORTH_MOM = NET_WORTH - NET_WORTH_LAST_MONTH; // +34,510
const NET_WORTH_MOM_PCT = ((NET_WORTH_MOM / NET_WORTH_LAST_MONTH) * 100);

// 12-month trend (thousands AED) — ends at current NET_WORTH (~699K).
const NW_TREND = [529, 545, 558, 572, 569, 585, 606, 618, 629, 645, 666, 699];
// 28 daily points across April '26 (thousands AED)
const NW_TREND_MONTH = [
  667, 669, 668, 671, 672, 674, 675, 673, 676, 679,
  681, 680, 683, 685, 687, 689, 688, 686, 689, 691,
  692, 694, 695, 693, 696, 697, 696, 699,
];
// 7 daily points — last week (thousands AED)
const NW_TREND_WEEK = [695, 693, 696, 697, 696, 695, 699];

// Period selector for the Net-Worth hero card
const NW_PERIODS = {
  week:  { trend: NW_TREND_WEEK,  delta:   4_120, pct:  0.59, label: '7 DAYS',  axis: ['MON',     'SUN'    ], caption: 'this week'  },
  month: { trend: NW_TREND_MONTH, delta:  34_510, pct:  5.20, label: '30 DAYS', axis: ['1 APR',   '28 APR' ], caption: 'this month' },
  year:  { trend: NW_TREND,       delta: 169_420, pct: 32.02, label: '12 MO',   axis: ["MAY '25", "APR '26"], caption: 'this year'  },
};

// Bank accounts
const BANK_ACCOUNTS = [
  { id:'enbd1', bank:'Emirates NBD',   type:'Current',  last:'•• 4821', balance: 68_420, synced:'2 min ago', region:'UAE' },
  { id:'adcb1', bank:'ADCB',           type:'Savings',  last:'•• 9102', balance: 92_100, synced:'14 min ago', region:'UAE' },
  { id:'fab1',  bank:'FAB',            type:'Salary',   last:'•• 0337', balance: 18_240, synced:'2 min ago', region:'UAE' },
  { id:'wise1', bank:'Wise',           type:'Multi-currency', last:'GBP 2,140 · USD 3,820', balance: 7_480, synced:'1 hr ago', region:'International' },
];

// Brokerages
const BROKERAGES = [
  { id:'saxo', name:'Saxo Bank', portfolio: 184_220, pnlDay: +1_842, pnlPct: +1.01, holdings: 14 },
  { id:'ibkr', name:'Interactive Brokers', portfolio: 98_640, pnlDay: -312, pnlPct: -0.32, holdings: 9 },
  { id:'etoro',name:'eToro', portfolio: 29_620, pnlDay: +180, pnlPct: +0.61, holdings: 6 },
];

// Crypto
const CRYPTO = [
  { id:'btc', symbol:'BTC', name:'Bitcoin',  qty: 0.62,    price: 98_420, value: 61_020, pnl24: +2.1 },
  { id:'eth', symbol:'ETH', name:'Ethereum', qty: 6.8,     price: 3_420,  value: 23_256, pnl24: -0.8 },
  { id:'sol', symbol:'SOL', name:'Solana',   qty: 58,      price: 172,    value: 9_976,  pnl24: +4.2 },
  { id:'usdc',symbol:'USDC',name:'USD Coin', qty: 48,      price: 1.00,   value: 48,     pnl24: 0 },
];

// Real estate — `value` is market value. Mortgage and equity are derived from
// LIABILITIES.mortgages via getMortgageForProperty(propertyId).
const REAL_ESTATE = [
  { id:'re1', name:'JVC Studio', location:'Jumeirah Village Circle, Dubai', value: 620_000, purchase: 540_000 },
];

// Liabilities — multi-level: Loans → (personal/car/student), Credit Cards, Mortgages.
const LIABILITIES = {
  loans: [
    { id:'l-pers', kind:'personal', lender:'Mashreq Personal Loan', balance: 18_400, originalAmount: 35_000, apr: 6.8, monthlyPayment:   920, payoffDate:'Nov 2027' },
    { id:'l-stud', kind:'student',  lender:'UK Student Loans',      balance: 32_400, originalAmount: 78_000, apr: 4.2, monthlyPayment:   380, payoffDate:'Mar 2030' },
    { id:'l-car',  kind:'car',      lender:'ADCB Auto Finance',     balance: 24_800, originalAmount: 48_000, apr: 5.9, monthlyPayment: 1_240, payoffDate:'Aug 2027' },
  ],
  credit_cards: [
    { id:'cc-enbd', issuer:'Emirates NBD', last:'•• 4821', balance: 4_420, limit: 30_000, apr: 36.0, minPayment: 220, statementDate: '5th' },
    { id:'cc-adcb', issuer:'ADCB',         last:'•• 9102', balance: 2_860, limit: 25_000, apr: 33.0, minPayment: 145, statementDate: '12th' },
  ],
  mortgages: [
    { id:'m-jvc', propertyId:'re1', lender:'Mashreq', balance: 460_000, originalAmount: 480_000, apr: 4.5, monthlyPayment: 2_840, term: '25 years', originationDate: 'May 2024' },
  ],
};

// Loan kind labels (for sub-header grouping under Loans)
const LOAN_KIND_LABELS = { personal: 'Personal Loan', student: 'Student Loan', car: 'Car Loan' };

function getMortgageForProperty(propertyId) {
  return LIABILITIES.mortgages.find(m => m.propertyId === propertyId) || null;
}

function getPropertyEquity(property) {
  const m = getMortgageForProperty(property.id);
  const mortgage = m ? m.balance : 0;
  return { equity: property.value - mortgage, mortgage };
}

// Other valuables
const OTHER_ASSETS = [
  { id:'car', name:'2022 Mazda CX-5', category:'Vehicle', value: 82_000, updated:'Mar 2026' },
  { id:'jewel', name:'Family Gold (22k)', category:'Jewellery', value: 46_000, updated:'Jan 2026' },
  { id:'biz', name:'Side Studio Equity', category:'Business', value: 22_400, updated:'Mar 2026' },
];

// Cash flow — 2-stage Sankey: income sources → category buckets.
// Pre-aggregated for the demo (TRANSACTIONS shows ~17 days of April; food etc.
// use the fuller-month figures from USER_MONTHLY_SPEND so the picture is honest).
// Totals balance: in = 25,642  →  out + saved = 25,642.
const CASH_FLOW = {
  in: [
    { key:'salary',    label:'Salary',     value: 24_800, color:'#9BB08A' },
    { key:'dividends', label:'Dividends',  value:    842, color:'#D4B27A' },
  ],
  out: [
    { key:'investments', label:'Investments', value:  3_200, color:'#D4B27A' },
    { key:'food',        label:'Food',        value:  1_840, color:'#9BB08A' },
    { key:'bills',       label:'Bills',       value:  1_011, color:'#D4A25A' },
    { key:'shopping',    label:'Shopping',    value:    433, color:'#C9B088' },
    { key:'transport',   label:'Transport',   value:    134, color:'#7389A8' },
    { key:'transfers',   label:'Transfers',   value:  5_000, color:'#E8D4A8' },
    { key:'saved',       label:'Saved',       value: 14_024, color:'#A8B8C5' },
  ],
};

// Recent activity (home feed)
const RECENT_ACTIVITY = [
  { id:'a1', icon:'Income',      title:'Salary — Meraki Media', account:'FAB Salary',    amount:+24_800,  when:'Today · 09:02' },
  { id:'a2', icon:'Food',        title:'Spinneys Dubai Marina', account:'Emirates NBD',  amount:-342.50,  when:'Today · 08:14' },
  { id:'a3', icon:'Transport',   title:'Careem',                account:'Emirates NBD',  amount:-48.00,   when:'Yesterday · 21:47' },
  { id:'a4', icon:'Investments', title:'Saxo — VWRL buy',       account:'Saxo Bank',     amount:-3_200,   when:'Yesterday · 17:22' },
  { id:'a5', icon:'Bills',       title:'DEWA',                  account:'ADCB Savings',  amount:-612.00,  when:'15 Apr · 11:30' },
];

// Today (used for budget pacing, forecasting, and goal time-remaining).
// Anchored to latest tx in the demo.
const TODAY = { day: 17, month: 4, monthName: 'April', monthShort: 'Apr', daysInMonth: 30, year: 2026 };

const MONTH_NAMES_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];

function parseMonthYear(str) {
  // "Jun 2027" → Date(2027, 5, 1)
  if (!str) return null;
  const parts = str.split(' ');
  const m = MONTH_NAMES_SHORT.indexOf(parts[0]);
  const y = parseInt(parts[1], 10);
  if (m < 0 || isNaN(y)) return null;
  return new Date(y, m, 1);
}

function fmtMonthYear(date) {
  if (!date) return '—';
  return MONTH_NAMES_SHORT[date.getMonth()] + ' ' + date.getFullYear();
}

function addMonths(date, n) {
  const d = new Date(date);
  d.setMonth(d.getMonth() + n);
  return d;
}

// Monthly budget caps (AED). Only spend categories — Transfers and Investments are
// savings flows, tracked separately as savings rate, not as a budget cap.
const BUDGETS = {
  Food:      1_500,
  Transport:   600,
  Shopping:    800,
  Bills:     1_400,
};
const BUDGET_CATEGORIES = Object.keys(BUDGETS);
const BUDGET_TOTAL = Object.values(BUDGETS).reduce((s,v) => s + v, 0); // 4,300

// Trailing 3-month average per category — used to seed budgets for first-time users.
const SPEND_3MO_AVG = {
  Food:      1_580,
  Transport:   615,
  Shopping:    790,
  Bills:     1_310,
};

function getMonthSpendByCategory(transactions = TRANSACTIONS) {
  const out = {};
  for (const c of BUDGET_CATEGORIES) out[c] = 0;
  for (const t of transactions) {
    if (t.amount >= 0) continue;
    if (BUDGET_CATEGORIES.includes(t.category)) out[t.category] += Math.abs(t.amount);
  }
  return out;
}

function projectMonthEnd(spentSoFar, day = TODAY.day, daysInMonth = TODAY.daysInMonth) {
  return spentSoFar * (daysInMonth / day);
}

// Historical full-month spend totals — used for forecast avg / min / max comparisons.
// Last 6 full months ending at March '26. April '26 is the current month and excluded.
const SPEND_HISTORY_MONTHS = [
  { month: 'Oct 2025', total: 7_840 },
  { month: 'Nov 2025', total: 8_950 },
  { month: 'Dec 2025', total: 11_220 },
  { month: 'Jan 2026', total: 7_120 },
  { month: 'Feb 2026', total: 8_310 },
  { month: 'Mar 2026', total: 9_460 },
];

// Stats for the forecast comparison rows: avg / min / max across SPEND_HISTORY_MONTHS,
// scaled to (i) the day-1→TODAY.day window and (ii) the TODAY.day+1→month-end window.
function getForecastWindowStats() {
  const totals = SPEND_HISTORY_MONTHS.map(m => m.total);
  const n      = totals.length;
  const sum    = totals.reduce((s,v) => s + v, 0);
  const avg    = sum / n;
  const min    = Math.min(...totals);
  const max    = Math.max(...totals);

  const dayFrac      = TODAY.day / TODAY.daysInMonth;
  const remainingFrac = 1 - dayFrac;

  return {
    monthsCount: n,
    soFar: {
      avg: avg * dayFrac,
      min: min * dayFrac,
      max: max * dayFrac,
    },
    remaining: {
      avg: avg * remainingFrac,
      min: min * remainingFrac,
      max: max * remainingFrac,
    },
  };
}

function getBudgetSnapshot(transactions = TRANSACTIONS, budgets = BUDGETS) {
  const spent = getMonthSpendByCategory(transactions);
  const items = BUDGET_CATEGORIES.map(cat => {
    const sp = spent[cat];
    const cap = budgets[cat];
    const pj = projectMonthEnd(sp);
    return {
      category: cat,
      spent: sp,
      budget: cap,
      projected: pj,
      pct: sp / cap,
      projectedPct: pj / cap,
      over: pj > cap,
      remainingToCap: cap - sp,
    };
  });
  const totalSpent     = items.reduce((s,r) => s + r.spent, 0);
  const totalBudget    = Object.values(budgets).reduce((s,v) => s + v, 0);
  const totalProjected = projectMonthEnd(totalSpent);
  const expectedSoFar  = totalBudget * (TODAY.day / TODAY.daysInMonth);
  return {
    today: TODAY,
    items,
    totalSpent, totalBudget, totalProjected,
    overUnder: totalProjected - totalBudget,
    paceDeltaPct: ((totalSpent - expectedSoFar) / expectedSoFar) * 100,
    onPace: totalSpent <= expectedSoFar,
  };
}

// ── Goals & Milestones ────────────────────────────────────────────────────
//
// Three goal kinds:
//   save     — accumulate AED toward a target (down payment, emergency fund, Hajj…)
//   payoff   — drive a linked liability balance to zero
//   networth — long-horizon "AED X by Year" milestone

const GOALS = [
  {
    id: 'g-dp', kind: 'save', icon: 'home', template: 'down_payment',
    title: 'House down payment', sub: 'For a 1.4M studio in JBR',
    targetAmount: 350_000, targetDate: 'Jun 2027',
    linkedAssetIds: ['adcb1', 'wise1'],   // ADCB Savings + Wise
    currentAmount: 99_580,                // earmarked subset of those balances
    monthlyContribution: 5_200,
    createdAt: 'Jan 2026', status: 'active',
  },
  {
    id: 'g-emerg', kind: 'save', icon: 'shield', template: 'emergency',
    title: 'Emergency fund', sub: '6 months of expenses',
    targetAmount: 50_000, targetDate: 'Feb 2027',
    linkedAssetIds: ['fab1'],
    currentAmount: 18_240,
    monthlyContribution: 4_000,
    createdAt: 'Feb 2026', status: 'active',
  },
  {
    id: 'g-stud', kind: 'payoff', icon: 'briefcase', template: 'payoff_student',
    title: 'Pay off student loan', sub: 'UK Student Loans · started AED 78K',
    startingAmount: 78_000, targetDate: 'Mar 2030',
    linkedLiabilityIds: ['l-stud'],
    monthlyContribution: 750,
    createdAt: 'Jan 2026', status: 'active',
  },
];

// Templates for the add-goal sheet.
const GOAL_TEMPLATES = [
  { id: 'down_payment',    kind: 'save',     icon: 'home',      label: 'Home down payment', defaultAmount: 350_000, defaultMonths: 18, sub: '20–25% of target property' },
  { id: 'emergency',       kind: 'save',     icon: 'shield',    label: 'Emergency fund',    defaultAmount:  50_000, defaultMonths: 12, sub: '3–6 months of expenses' },
  { id: 'hajj',            kind: 'save',     icon: 'star',      label: 'Hajj',              defaultAmount:  50_000, defaultMonths: 24, sub: 'Pilgrimage savings' },
  { id: 'education',       kind: 'save',     icon: 'briefcase', label: 'Education',         defaultAmount: 150_000, defaultMonths: 36, sub: 'Self or kids' },
  { id: 'travel',          kind: 'save',     icon: 'arrowUR',   label: 'Travel fund',       defaultAmount:  25_000, defaultMonths: 12, sub: 'Dream trip savings' },
  { id: 'payoff_debt',     kind: 'payoff',   icon: 'wallet',    label: 'Pay off a debt',                                              sub: 'Pick a loan, card, or mortgage' },
  { id: 'networth_target', kind: 'networth', icon: 'sparkles',  label: 'Net worth target',  defaultAmount: 1_000_000, defaultMonths: 60, sub: 'Long-term wealth milestone' },
  { id: 'custom',          kind: 'save',     icon: 'target',    label: 'Custom goal',                                                 sub: 'Set your own target' },
];

const GOAL_KIND_LABELS = { save: 'Save toward', payoff: 'Pay off', networth: 'Net worth' };

// Look up the linked liability balance for a payoff goal.
function getGoalLinkedLiabilityBalance(goal) {
  const ids = goal.linkedLiabilityIds || [];
  let bal = 0;
  for (const id of ids) {
    for (const l of LIABILITIES.loans)        if (l.id === id) bal += l.balance;
    for (const c of LIABILITIES.credit_cards) if (c.id === id) bal += c.balance;
    for (const m of LIABILITIES.mortgages)    if (m.id === id) bal += m.balance;
  }
  return bal;
}

function getGoalLinkedAccounts(goal) {
  const ids = goal.linkedAssetIds || [];
  const out = [];
  for (const id of ids) {
    const b = BANK_ACCOUNTS.find(x => x.id === id);
    if (b) out.push({ kind: 'bank', id: b.id, name: b.bank, sub: `${b.type} · ${b.last}`, balance: b.balance });
  }
  if (goal.linkedLiabilityIds) {
    for (const id of goal.linkedLiabilityIds) {
      const all = [...LIABILITIES.loans, ...LIABILITIES.credit_cards, ...LIABILITIES.mortgages];
      const l = all.find(x => x.id === id);
      if (l) out.push({ kind: 'liability', id: l.id, name: l.lender || l.issuer, sub: `${(l.apr || 0).toFixed(1)}% APR`, balance: l.balance });
    }
  }
  return out;
}

// Compute progress, pacing, and projected completion for a goal.
function getGoalMetrics(goal) {
  const now = new Date(TODAY.year, TODAY.month - 1, TODAY.day);
  const target_d = parseMonthYear(goal.targetDate) || addMonths(now, 12);
  const monthsRemaining = Math.max(1, Math.round((target_d - now) / (1000 * 60 * 60 * 24 * 30.44)));

  let current, target, distance, progressPct;
  if (goal.kind === 'save') {
    current = goal.currentAmount ?? 0;
    target = goal.targetAmount;
    distance = Math.max(0, target - current);
    progressPct = Math.max(0, Math.min(1, current / target));
  } else if (goal.kind === 'payoff') {
    const remaining = getGoalLinkedLiabilityBalance(goal);
    current = remaining;
    target = 0;
    distance = remaining;
    const start = goal.startingAmount || remaining;
    progressPct = Math.max(0, Math.min(1, (start - remaining) / start));
  } else if (goal.kind === 'networth') {
    current = NET_WORTH;
    target = goal.targetAmount;
    distance = Math.max(0, target - current);
    progressPct = Math.max(0, Math.min(1, current / target));
  }

  const requiredMonthly = distance / monthsRemaining;
  const contrib = goal.monthlyContribution || 0;
  const monthsAtCurrentPace = contrib > 0 ? Math.ceil(distance / contrib) : null;
  const projectedCompletionDate = monthsAtCurrentPace != null ? addMonths(now, monthsAtCurrentPace) : null;
  const onPace = contrib >= requiredMonthly;

  return {
    current, target, distance, progressPct,
    monthsRemaining, requiredMonthly,
    monthlyContribution: contrib,
    monthsAtCurrentPace, projectedCompletionDate,
    onPace,
    targetDate: target_d,
  };
}

// Transactions (wider feed)
const CATEGORIES = ['All','Food','Transport','Shopping','Bills','Transfers','Income','Investments'];

const TRANSACTIONS = [
  { id:'t1',  merchant:'Salary — Meraki Media', category:'Income',     account:'FAB',          amount:+24_800, date:'17 Apr' },
  { id:'t2',  merchant:'Spinneys Dubai Marina', category:'Food',       account:'Emirates NBD', amount:-342.50, date:'17 Apr' },
  { id:'t3',  merchant:'Careem',                category:'Transport',  account:'Emirates NBD', amount:-48.00,  date:'17 Apr' },
  { id:'t4',  merchant:'Saxo — VWRL',            category:'Investments',account:'Saxo',        amount:-3_200,  date:'16 Apr' },
  { id:'t5',  merchant:'Noon',                   category:'Shopping',   account:'Emirates NBD',amount:-287.00, date:'16 Apr' },
  { id:'t6',  merchant:'DEWA',                   category:'Bills',      account:'ADCB',        amount:-612.00, date:'15 Apr' },
  { id:'t7',  merchant:'Careem',                 category:'Transport',  account:'Emirates NBD',amount:-62.00,  date:'15 Apr' },
  { id:'t8',  merchant:'Transfer to Savings',    category:'Transfers',  account:'Emirates NBD → ADCB', amount:-5_000, date:'14 Apr' },
  { id:'t9',  merchant:'Deliveroo',              category:'Food',       account:'Emirates NBD',amount:-94.50,  date:'14 Apr' },
  { id:'t10', merchant:'Starbucks City Walk',    category:'Food',       account:'Emirates NBD',amount:-32.00,  date:'14 Apr' },
  { id:'t11', merchant:'du',                     category:'Bills',      account:'ADCB',        amount:-399.00, date:'13 Apr' },
  { id:'t12', merchant:'The Sustainable City',   category:'Shopping',   account:'Emirates NBD',amount:-146.00, date:'13 Apr' },
  { id:'t13', merchant:'RTA Salik',              category:'Transport',  account:'Emirates NBD',amount:-24.00,  date:'12 Apr' },
  { id:'t14', merchant:'Dividend — VWRL',        category:'Income',     account:'Saxo',        amount:+842.10, date:'12 Apr' },
  { id:'t15', merchant:'Carrefour Mall of Emirates', category:'Food',   account:'Emirates NBD',amount:-418.20, date:'11 Apr' },
];

// AI recommendations
const RECOMMENDATIONS = [
  {
    id:'r1', category:'Savings',
    impact:'High',
    headline:'AED 45K sitting idle in your current account',
    body:'At 0% in Emirates NBD Current, this drags on your return. A FAB High-Yield Saver at 4.2% APY would add ~AED 1,890/yr with same-day liquidity.',
    cta:'Explore high-yield options',
    meta:'Potential: +AED 1,890/yr',
  },
  {
    id:'r2', category:'Investments',
    impact:'Medium',
    headline:'Crypto is 38% of liquid net worth — above typical risk profile',
    body:'Your profile (moderate, age 31, 10-yr horizon) suggests 10–15% crypto. Rebalancing ~AED 65K into your Saxo world index would reduce drawdown risk.',
    cta:'Simulate rebalance',
    meta:'Target: 12% crypto allocation',
  },
  {
    id:'r3', category:'Property',
    impact:'Medium',
    headline:'You qualify for a mortgage in ~14 months at current savings rate',
    body:'With AED 5.2K/mo going to savings and a 25% DP on a AED 1.4M property, you\'re 14 months from pre-approval. Adjust lifestyle spend to shave 4 months.',
    cta:'Run mortgage scenarios',
    meta:'Timeline: Jun 2027',
  },
  {
    id:'r4', category:'Protection',
    impact:'Low',
    headline:'No life cover detected — worth reviewing',
    body:'You have dependents-adjacent assets (mortgage, family support). A term life policy at your age is ~AED 90/mo for AED 1M cover.',
    cta:'Compare term policies',
    meta:'From AED 90/mo',
  },
  {
    id:'r5', category:'Investments',
    impact:'Low',
    headline:'You\'re paying Saxo platform fees on dormant cash',
    body:'AED 8.2K uninvested in Saxo is costing ~0.25%/yr. Sweep to Saxo money market fund (currently 4.8%) or back to Wise.',
    cta:'Sweep cash',
    meta:'+AED 360/yr',
  },
];

// Financial literacy — short-read articles surfaced as personalized hook tiles on home
// and in the dedicated Learn library. `relevance` keys are derived from portfolio signals
// in getRelevantLearnArticles() below — 'always' means it shows for every user.
const LEARN_ARTICLES = [
  {
    id: 'la-apy-basics',
    category: 'Saving',
    title: 'What is APY, really?',
    summary: 'Why a 4.2% high-yield account beats your 0% current.',
    readMinutes: 3,
    relevance: ['idle-cash'],
    relatedRecId: 'r1',
    body: [
      { kind: 'p', text: 'APY — Annual Percentage Yield — is the rate your money actually earns over a year, including the effect of compounding. APR doesn\'t. That difference is small at low rates, large at high ones.' },
      { kind: 'h2', text: 'How it compounds' },
      { kind: 'p', text: 'Most UAE high-yield savers credit interest monthly. AED 45,000 at 4.2% APY earns AED 1,890 over a year — roughly AED 157 a month — with no lockup and same-day liquidity if you keep the account in good standing.' },
      { kind: 'h2', text: 'What to watch' },
      { kind: 'p', text: 'Headline rates often require a salary transfer or a minimum balance. Check the fine print: tiered rates can mean only the first AED 100K earn the top rate, and the rest drop to 1–2%.' },
    ],
    takeaways: [
      'APY includes compounding; APR does not.',
      'AED 45K idle at 4.2% earns ~AED 1,890/year.',
      'Tiered rates often cap the top rate at the first AED 100K.',
    ],
  },
  {
    id: 'la-hysa-uae',
    category: 'Saving',
    title: 'High-yield savings in the UAE',
    summary: 'How FAB, ADCB, ENBD and Wio compare on rate, lockup, and minimums.',
    readMinutes: 4,
    relevance: ['idle-cash'],
    relatedRecId: 'r1',
    body: [
      { kind: 'p', text: 'A handful of UAE banks now offer rates above 4% APY on AED savings, but the structure varies. Some require a salary transfer; others a minimum balance; a few pay top rates only on the first AED 100K.' },
      { kind: 'h2', text: 'The shortlist' },
      { kind: 'p', text: 'FAB iSave and Wio Saving Spaces are the two that come up most for digital-first users — both pay around 4.0–4.2%, no lockup, with monthly interest credit. ADCB Hayyak Saver and ENBD Smart Saver are close behind but typically tie the rate to a salary transfer.' },
      { kind: 'h2', text: 'The trade-off' },
      { kind: 'p', text: 'Higher rates almost always come with a behavioural condition (salary, no withdrawals in a month, etc.). Read the schedule of charges before moving — a missed condition can drop the effective rate to 0.5%.' },
    ],
    takeaways: [
      'Top UAE digital savers pay ~4.0–4.2% APY.',
      'Most top rates require salary transfer or minimum balance.',
      'Tiered rates cap the headline at the first AED 100K.',
    ],
  },
  {
    id: 'la-crypto-risk',
    category: 'Crypto',
    title: 'Crypto risk basics: drawdown vs. return',
    summary: 'Why a 12% return at 2.4× the drawdown is not really winning.',
    readMinutes: 4,
    relevance: ['crypto-heavy'],
    relatedRecId: 'r2',
    body: [
      { kind: 'p', text: 'Returns alone tell you nothing about how an asset behaves. Drawdown — the peak-to-trough drop along the way — tells you what you\'d have lived through to get the return.' },
      { kind: 'h2', text: 'Why it matters' },
      { kind: 'p', text: 'Crypto can return more than equities over a multi-year horizon, but with drawdowns of 50–80%. If a 50% paper loss would force you to sell — or just keep you up at night — your real risk capacity is lower than your portfolio suggests.' },
      { kind: 'h2', text: 'The 10–15% rule of thumb' },
      { kind: 'p', text: 'A common framing for moderate-risk investors with a 10-year horizon is to cap crypto at 10–15% of liquid net worth. Above that, you\'re no longer diversifying — you\'re concentrated.' },
    ],
    takeaways: [
      'Returns without drawdown context are misleading.',
      'Crypto drawdowns of 50–80% are normal, not anomalous.',
      'A 10–15% cap on crypto is the typical moderate-risk framing.',
    ],
  },
  {
    id: 'la-rebalancing',
    category: 'Investing',
    title: 'Why rebalancing matters',
    summary: 'How a 12% target turns into 38% if you never touch it.',
    readMinutes: 3,
    relevance: ['crypto-heavy'],
    body: [
      { kind: 'p', text: 'Rebalancing is the discipline of selling what has grown past its target weight and buying what has fallen below. It sounds counterintuitive — why sell winners? — but it\'s the mechanism that keeps your risk profile stable as markets move.' },
      { kind: 'h2', text: 'Drift is silent' },
      { kind: 'p', text: 'A portfolio set at 60/30/10 (stocks/bonds/crypto) can drift to 50/15/35 in a strong crypto year without you doing anything. Your stated risk tolerance hasn\'t changed; your actual exposure has.' },
      { kind: 'h2', text: 'How often' },
      { kind: 'p', text: 'Most retail investors rebalance once or twice a year, or when an asset class drifts more than 5 percentage points from target. More frequent rebalancing rarely improves returns and adds tax friction.' },
    ],
    takeaways: [
      'Rebalancing keeps risk stable as markets move.',
      'Drift is silent — your "10% crypto" can become 35% on its own.',
      'Once or twice a year, or at >5pp drift, is enough.',
    ],
  },
  {
    id: 'la-uae-mortgage',
    category: 'Property',
    title: 'UAE mortgages: 25% down, explained',
    summary: 'What expats need to know before pre-approval.',
    readMinutes: 5,
    relevance: ['mortgage-saver'],
    relatedRecId: 'r3',
    body: [
      { kind: 'p', text: 'For expat residents buying a first home in the UAE, the Central Bank rule is 20% down on properties up to AED 5M and 30% on those above. Add 4% Dubai Land Department fees, 2% agent commission, and AED 5–10K in admin and you\'re closer to a 25–28% upfront cost.' },
      { kind: 'h2', text: 'What banks look for' },
      { kind: 'p', text: 'Stable salary for 6+ months, debt-burden ratio (DBR) under 50%, and a clean Al Etihad Credit Bureau score. Banks will pre-approve you for 7× annual salary as a rough cap.' },
      { kind: 'h2', text: 'Fixed vs. variable' },
      { kind: 'p', text: 'Fixed-rate offers in the UAE typically lock for 1–5 years, then revert to a variable rate tied to EIBOR + a margin. Compare the post-fixed reversion rate, not just the headline.' },
    ],
    takeaways: [
      '20–30% down per Central Bank, plus ~6% in fees on top.',
      'Banks pre-approve up to ~7× annual salary, DBR under 50%.',
      'Always compare the post-fixed reversion rate, not just the teaser.',
    ],
  },
  {
    id: 'la-term-life',
    category: 'Protection',
    title: 'Term life cover at 30',
    summary: 'When it\'s worth it, when it isn\'t, and what AED 90/mo actually buys.',
    readMinutes: 3,
    relevance: ['no-life-cover'],
    relatedRecId: 'r4',
    body: [
      { kind: 'p', text: 'Term life pays a lump sum to your dependents if you die during the policy period. It\'s the cheapest form of cover because it has no investment component — just pure insurance.' },
      { kind: 'h2', text: 'When to consider it' },
      { kind: 'p', text: 'If anyone — a partner, parents, children — depends on your income, or if you have a mortgage that would otherwise force a property sale, term life is worth pricing. If you have no dependents and no debts, it usually isn\'t.' },
      { kind: 'h2', text: 'What it costs' },
      { kind: 'p', text: 'A healthy 30-year-old non-smoker in the UAE can typically get AED 1M in cover for around AED 90/month over a 20-year term. Smokers and those with pre-existing conditions pay 2–3×.' },
    ],
    takeaways: [
      'Term life is the cheapest cover — no investment component.',
      'Consider it if anyone depends on your income.',
      'Healthy 30yo: ~AED 90/mo for AED 1M over 20 years.',
    ],
  },
  {
    id: 'la-etf-vs-fund',
    category: 'Investing',
    title: 'ETF vs. mutual fund',
    summary: 'Why most retail investors should default to ETFs.',
    readMinutes: 3,
    relevance: ['always'],
    body: [
      { kind: 'p', text: 'Both pool money from many investors into a basket of holdings. The differences sit in how you buy them, what they cost, and how transparent they are.' },
      { kind: 'h2', text: 'How they trade' },
      { kind: 'p', text: 'ETFs trade on an exchange, like a stock — you see the price live and can buy or sell any time the market is open. Mutual funds price once a day, after market close, and you transact at that single end-of-day NAV.' },
      { kind: 'h2', text: 'Cost' },
      { kind: 'p', text: 'Index ETFs typically charge 0.05–0.20% per year. Actively-managed mutual funds charge 1–2%. Over 20 years, that gap can eat 30%+ of your end balance.' },
    ],
    takeaways: [
      'ETFs trade live; mutual funds price once a day.',
      'Index ETFs cost 0.05–0.20%; active funds 1–2%.',
      'Fees compound — small differences become large over decades.',
    ],
  },
  {
    id: 'la-uae-tax',
    category: 'Tax',
    title: 'Tax in the UAE: what residents owe',
    summary: 'No personal income tax — but corporate tax, VAT, and home-country rules still bite.',
    readMinutes: 4,
    relevance: ['always'],
    body: [
      { kind: 'p', text: 'The UAE has no personal income tax on salary or most investment income for residents. That\'s the headline — but it isn\'t the whole story.' },
      { kind: 'h2', text: 'What you do pay' },
      { kind: 'p', text: '5% VAT on most goods and services. 9% corporate tax on business profits above AED 375K (since 2023). Excise on tobacco, energy drinks, and sweetened beverages. Property registration and transfer fees on real estate.' },
      { kind: 'h2', text: 'Home-country rules' },
      { kind: 'p', text: 'US citizens are taxed on worldwide income regardless of residence. UK and Canadian expats can usually break tax residency, but rules on returning are strict. Always check your home country\'s residence test before assuming UAE-only liability.' },
    ],
    takeaways: [
      'No personal income tax for residents — salary and most investment income.',
      '5% VAT, 9% corporate tax above AED 375K, plus excise duties.',
      'US citizens still owe US tax on worldwide income.',
    ],
  },
];

function getRelevantLearnArticles(limit = 5) {
  const signals = new Set(['always']);
  const liquid = ASSET_BREAKDOWN
    .filter(s => !['realestate','other'].includes(s.key))
    .reduce((s,x) => s + x.value, 0);
  const cryptoSeg = ASSET_BREAKDOWN.find(s => s.key === 'crypto');
  if (cryptoSeg && (cryptoSeg.value / liquid) * 100 > 25) signals.add('crypto-heavy');
  if (RECOMMENDATIONS.some(r => r.id === 'r1')) signals.add('idle-cash');
  if (RECOMMENDATIONS.some(r => r.id === 'r3')) signals.add('mortgage-saver');
  if (RECOMMENDATIONS.some(r => r.id === 'r4')) signals.add('no-life-cover');
  return LEARN_ARTICLES
    .filter(a => a.relevance.some(r => signals.has(r)))
    .slice(0, limit);
}

// AI chat — sample prompts + scripted responses
const CHAT_PROMPTS = [
  "What's my net worth across everything?",
  "How is my crypto performing vs my stocks?",
  "Am I liquid enough for a property down payment?",
  "Where did I overspend this month?",
];

// Scripted AI responses (by prompt keyword)
const AI_SCRIPT = {
  networth: {
    text: "Your total net worth is AED 698,540 — up 5.2% month-over-month. Here's the breakdown:",
    card: { type: 'breakdown' },
    follow: "The biggest contributor this month was your Saxo portfolio (+AED 18.4K), driven by the world index ETF. Mortgage paydown also added AED 2.9K.",
  },
  crypto: {
    text: "Over the last 90 days, your crypto is up 12.8% while your stock portfolio is up 6.2%. But with a 2.4× higher drawdown.",
    card: { type: 'compare', rows: [
      { label: 'Crypto', value: '+12.8%', sub: 'AED 94.3K · 38% of liquid NW', tone: 'pos' },
      { label: 'Stocks', value: '+6.2%',  sub: 'AED 312.5K · via Saxo/IBKR',    tone: 'pos' },
    ]},
    follow: "Risk-adjusted, stocks are outperforming. Want me to model a rebalance?",
  },
  liquid: {
    text: "For a AED 1.4M property at 25% down (AED 350K), you'd need to free up cash. Here's where you are today:",
    card: { type: 'liquidity', ready: 186_240, target: 350_000, eta: '~14 months' },
    follow: "You're 53% of the way there. At your current AED 5.2K/mo savings rate, you'd hit the DP by June 2027.",
  },
  spend: {
    text: "You spent AED 8,420 this month — about 11% over your 3-month average. Food and Shopping drove most of it.",
    card: { type: 'spend', items: [
      { label: 'Food & Dining', value: 2_840, delta: +420 },
      { label: 'Shopping',      value: 1_920, delta: +380 },
      { label: 'Transport',     value:   612, delta: -90 },
      { label: 'Bills',         value: 1_011, delta: +12 },
    ]},
    follow: "Spinneys and Noon account for AED 1.6K alone. Want to set a category cap?",
  },
};

// ── Peer comparison ────────────────────────────────────────────────────────
// User-facing cohort filter options. Order = display order in the customizer.
const COHORT_OPTIONS = {
  age:      [{id:'25-35', label:'25–35'}, {id:'35-45', label:'35–45'}, {id:'45-55', label:'45–55'}],
  gender:   [{id:'all', label:'All'}, {id:'female', label:'Women'}, {id:'male', label:'Men'}],
  city:     [{id:'dubai', label:'Dubai'}, {id:'auh', label:'Abu Dhabi'}, {id:'uae', label:'UAE-wide'}],
  income:   [{id:'<15k', label:'< AED 15K/mo'}, {id:'15-25k', label:'AED 15–25K/mo'}, {id:'25-50k', label:'AED 25–50K/mo'}, {id:'50k+', label:'AED 50K+/mo'}],
};

const COHORT_LABELS = (() => {
  const m = {};
  ['age','gender','city','income'].forEach(k => {
    COHORT_OPTIONS[k].forEach(o => { m[`${k}:${o.id}`] = o.label; });
  });
  return m;
})();

// Hand-tuned cohorts. Net-worth quartiles in AED; spending values are monthly AED.
// User's actual values (NET_WORTH derived above; spending below) stay constant — only
// the peer medians change per cohort.
const PEER_BENCHMARKS = {
  'female-25-35-dubai-15-25k': {
    label: 'Women · 25–35 · Dubai · AED 15–25K/mo',
    sample: 1284,
    netWorth: { p25: 145_000, p50: 312_000, p75: 540_000, p90: 820_000 },
    spending: {
      total:     7_800,
      housing:   4_050,
      food:      1_650,
      transport:   620,
      travel:      480,
    },
  },
  'female-25-35-dubai-25-50k': {
    label: 'Women · 25–35 · Dubai · AED 25–50K/mo',
    sample: 642,
    netWorth: { p25: 320_000, p50: 680_000, p75: 1_180_000, p90: 1_950_000 },
    spending: {
      total:     14_200,
      housing:   7_400,
      food:      2_400,
      transport:   980,
      travel:    1_320,
    },
  },
  'male-25-35-dubai-15-25k': {
    label: 'Men · 25–35 · Dubai · AED 15–25K/mo',
    sample: 1612,
    netWorth: { p25: 132_000, p50: 285_000, p75: 510_000, p90: 790_000 },
    spending: {
      total:     7_950,
      housing:   3_900,
      food:      1_580,
      transport:   780,
      travel:      520,
    },
  },
  'male-25-35-dubai-25-50k': {
    label: 'Men · 25–35 · Dubai · AED 25–50K/mo',
    sample: 814,
    netWorth: { p25: 340_000, p50: 720_000, p75: 1_240_000, p90: 2_100_000 },
    spending: {
      total:     14_800,
      housing:   7_100,
      food:      2_350,
      transport: 1_240,
      travel:    1_280,
    },
  },
  'female-35-45-dubai-25-50k': {
    label: 'Women · 35–45 · Dubai · AED 25–50K/mo',
    sample: 528,
    netWorth: { p25: 540_000, p50: 1_180_000, p75: 2_100_000, p90: 3_400_000 },
    spending: {
      total:     16_400,
      housing:   8_900,
      food:      2_900,
      transport: 1_180,
      travel:    1_650,
    },
  },
  'all-25-35-uae-15-25k': {
    label: 'All · 25–35 · UAE-wide · AED 15–25K/mo',
    sample: 4280,
    netWorth: { p25: 128_000, p50: 295_000, p75: 520_000, p90: 800_000 },
    spending: {
      total:     7_600,
      housing:   3_600,
      food:      1_540,
      transport:   720,
      travel:      490,
    },
  },
};

// User's actual monthly spending — used as the "you" side of every comparison.
// Hand-set for the demo since transaction categories don't include Housing/Travel.
const USER_MONTHLY_SPEND = {
  total:     8_420,
  housing:   5_200,   // rent
  food:      1_840,
  transport:   540,
  travel:    1_150,
};

const PEER_SPEND_LABELS = {
  total:     'Total monthly',
  housing:   'Housing',
  food:      'Food & groceries',
  transport: 'Transportation',
  travel:    'Travel',
};

// Resolve a cohort + compute the user's percentile in it.
// filters = { age, gender, city, income } — any field missing falls back to USER.
function getPeerComparison(filters = {}) {
  const f = {
    age:      filters.age      ?? USER.ageBracket,
    gender:   filters.gender   ?? USER.gender,
    city:     filters.city     ?? USER.city,
    income:   filters.income   ?? USER.incomeTier,
  };

  const exactId = `${f.gender}-${f.age}-${f.city}-${f.income}`;
  // Try exact, then progressively broader fallbacks.
  const candidateIds = [
    exactId,
    `${f.gender}-${f.age}-${f.city}-15-25k`,
    `${f.gender}-${f.age}-uae-${f.income}`,
    `all-${f.age}-${f.city}-${f.income}`,
    `all-${f.age}-uae-${f.income}`,
    'all-25-35-uae-15-25k',
  ];

  let cohort = null, resolvedId = null, isFallback = false;
  for (const id of candidateIds) {
    if (PEER_BENCHMARKS[id]) {
      cohort = PEER_BENCHMARKS[id];
      resolvedId = id;
      isFallback = id !== exactId;
      break;
    }
  }
  if (!cohort) cohort = Object.values(PEER_BENCHMARKS)[0];

  // Sample-size guard.
  if (cohort.sample < 200) {
    return { tooSmall: true, sample: cohort.sample, label: cohort.label, filters: f };
  }

  // Build a human label from the active filters (not the cohort's stored label,
  // so the pill always reflects what the user picked).
  const liveLabel = [
    COHORT_LABELS[`gender:${f.gender}`] || 'All',
    COHORT_LABELS[`age:${f.age}`],
    COHORT_LABELS[`city:${f.city}`],
    COHORT_LABELS[`income:${f.income}`],
  ].join(' · ');

  // Percentile via piecewise-linear interpolation across the quartile points.
  const nw = NET_WORTH;
  const { p25, p50, p75, p90 } = cohort.netWorth;
  let pct;
  if      (nw <= p25) pct = (nw / p25) * 25;
  else if (nw <= p50) pct = 25 + ((nw - p25) / (p50 - p25)) * 25;
  else if (nw <= p75) pct = 50 + ((nw - p50) / (p75 - p50)) * 25;
  else if (nw <= p90) pct = 75 + ((nw - p75) / (p90 - p75)) * 15;
  else                pct = Math.min(99, 90 + ((nw - p90) / (p90 * 0.6)) * 9);
  const percentile = Math.round(Math.max(1, Math.min(99, pct)));

  // Spending deltas (positive = user spends more than peer median).
  const spending = {};
  Object.keys(PEER_SPEND_LABELS).forEach(k => {
    const userVal = USER_MONTHLY_SPEND[k];
    const peerMed = cohort.spending[k];
    spending[k] = {
      key: k,
      label: PEER_SPEND_LABELS[k],
      user: userVal,
      peer: peerMed,
      deltaPct: ((userVal - peerMed) / peerMed) * 100,
      ratio: userVal / peerMed,
      above: userVal > peerMed,
    };
  });

  return {
    resolvedId,
    isFallback,
    requestedFilters: f,
    label: liveLabel,
    sample: cohort.sample,
    percentile,
    netWorth: { user: nw, p25, p50, p75, p90, peerMedian: p50 },
    spending,
  };
}

Object.assign(window, {
  USER, NET_WORTH, NET_WORTH_LAST_MONTH, NET_WORTH_MOM, NET_WORTH_MOM_PCT,
  ASSETS_TOTAL, ASSETS_LAST_MONTH,
  LIABILITIES, LIABILITY_BREAKDOWN, LIABILITIES_TOTAL, LIABILITIES_LAST_MONTH, LIABILITIES_MOM,
  LOAN_KIND_LABELS, getMortgageForProperty, getPropertyEquity,
  NW_TREND, NW_TREND_MONTH, NW_TREND_WEEK, NW_PERIODS,
  ASSET_BREAKDOWN, BANK_ACCOUNTS, BROKERAGES, CRYPTO, REAL_ESTATE, OTHER_ASSETS,
  CASH_FLOW,
  RECENT_ACTIVITY, CATEGORIES, TRANSACTIONS, RECOMMENDATIONS, CHAT_PROMPTS, AI_SCRIPT,
  TODAY, BUDGETS, BUDGET_CATEGORIES, BUDGET_TOTAL, SPEND_3MO_AVG,
  SPEND_HISTORY_MONTHS, getForecastWindowStats,
  getMonthSpendByCategory, projectMonthEnd, getBudgetSnapshot,
  GOALS, GOAL_TEMPLATES, GOAL_KIND_LABELS,
  getGoalMetrics, getGoalLinkedAccounts, getGoalLinkedLiabilityBalance,
  parseMonthYear, fmtMonthYear, addMonths, MONTH_NAMES_SHORT,
  NOTIFICATIONS, SUBSCRIPTION,
  PLAN_BENEFITS_PRO, ANNUAL_VALUE_RECAP, HELP_FAQS,
  LEARN_ARTICLES, getRelevantLearnArticles,
  COHORT_OPTIONS, COHORT_LABELS, PEER_BENCHMARKS, USER_MONTHLY_SPEND, PEER_SPEND_LABELS, getPeerComparison,
});
