// Rumbo — Default data + localStorage helpers

const NOW = new Date();
const CURRENT_MONTH = `${NOW.getFullYear()}-${String(NOW.getMonth()+1).padStart(2,'0')}`;
const PREV_MONTH = (() => {
  const d = new Date(NOW.getFullYear(), NOW.getMonth() - 1, 1);
  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
})();

// Fecha ISO relativa al mes actual: offset de meses + día (clampeado al largo del mes destino)
function demoDate(monthOffset, day) {
  const base = new Date(NOW.getFullYear(), NOW.getMonth() + monthOffset, 1);
  const lastDay = new Date(base.getFullYear(), base.getMonth() + 1, 0).getDate();
  const d = Math.min(day, lastDay);
  return `${base.getFullYear()}-${String(base.getMonth()+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
}

const DEFAULT_FX = { ARS: 1, USD: 1040, PEN: 280, MXN: 55, EUR: 1150, BRL: 200, UYU: 25, USDT: 1040 };

/* ── Esqueleto vacío — lo que recibe quien elige "Empezar de cero" ── */
const EMPTY_DATA = {
  accounts: [],
  debts: [],
  transactions: [],
  budgets: [],
  goals: [],
  customCategories: [],
  settings: { name: '', currency: 'ARS', monthlyIncome: 0, theme: 'light' },
  fxRates: { ...DEFAULT_FX },
  recurringIncomes: [],
  fixedExpenses: [],
  isDemo: false,
};

/* ── Datos de ejemplo — generados SIEMPRE relativos al mes actual, no se vencen nunca.
      Personas y montos ficticios, sin datos personales. ── */
function buildDemoData() {
  const today = NOW.getDate();
  const tx = [];
  let seq = 0;
  // El mes en curso (offset 0) solo muestra movimientos hasta hoy — como una cuenta real en uso
  const add = (mOff, d, type, category, description, amount, accountId) => {
    if (mOff === 0 && d > today) return;
    tx.push({ id: 'demo' + (++seq), date: demoDate(mOff, d), type, category, description, amount, accountId });
  };

  // Mes en curso
  add(0,  1, 'income',  'Sueldo',          'Sueldo del mes',          670000, 'a2');
  add(0,  2, 'expense', 'Servicios',       'Internet',                18000,  'a2');
  add(0,  4, 'expense', 'Comida',          'Supermercado',            38000,  'a1');
  add(0,  5, 'expense', 'Hogar',           'Expensas',                45000,  'a2');
  add(0,  7, 'expense', 'Transporte',      'SUBE + nafta',            16500,  'a1');
  add(0,  9, 'expense', 'Entretenimiento', 'Cine + cena',             28000,  'a1');
  add(0, 10, 'income',  'Freelance',       'Trabajo freelance',       95000,  'a3');
  add(0, 12, 'expense', 'Salud',           'Farmacia',                9500,   'a2');
  add(0, 13, 'expense', 'Comida',          'Verdulería + panadería',  12300,  'a1');
  add(0, 16, 'expense', 'Transporte',      'Uber',                    8200,   'a3');
  add(0, 19, 'expense', 'Comida',          'Delivery',                14500,  'a3');
  add(0, 22, 'expense', 'Deudas',          'Pago cuota Visa',         18000,  'a2');
  add(0, 25, 'expense', 'Entretenimiento', 'Streaming',               12400,  'a2');
  // Mes anterior — completo y variado
  add(-1,  1, 'income',  'Sueldo',          'Sueldo del mes',         650000, 'a2');
  add(-1,  3, 'expense', 'Servicios',       'Internet + celular',     28900,  'a2');
  add(-1,  5, 'expense', 'Comida',          'Supermercado',           42500,  'a1');
  add(-1,  8, 'expense', 'Transporte',      'SUBE + Uber',            18200,  'a1');
  add(-1, 10, 'expense', 'Entretenimiento', 'Streaming',              12400,  'a2');
  add(-1, 12, 'expense', 'Comida',          'Delivery',               15800,  'a2');
  add(-1, 15, 'expense', 'Deudas',          'Pago cuota Visa',        18000,  'a2');
  add(-1, 17, 'expense', 'Comida',          'Verdulería + panadería', 22100,  'a1');
  add(-1, 19, 'expense', 'Salud',           'Farmacia',               14300,  'a2');
  add(-1, 20, 'expense', 'Deudas',          'Pago préstamo',          15000,  'a2');
  add(-1, 21, 'expense', 'Entretenimiento', 'Salida cena + bar',      35600,  'a1');
  add(-1, 22, 'income',  'Freelance',       'Proyecto de diseño',     85000,  'a3');
  add(-1, 22, 'expense', 'Ropa',            'Zapatillas',             52000,  'a3');
  // -2
  add(-2,  1, 'income',  'Sueldo',          'Sueldo del mes',         620000, 'a2');
  add(-2,  5, 'expense', 'Comida',          'Supermercado',           98000,  'a1');
  add(-2, 10, 'expense', 'Transporte',      'Combustible',            25000,  'a2');
  add(-2, 15, 'expense', 'Entretenimiento', 'Cine + salida',          28000,  'a1');
  add(-2, 20, 'expense', 'Servicios',       'Luz + gas',              31000,  'a2');
  add(-2, 25, 'expense', 'Salud',           'Médico particular',      18000,  'a2');
  add(-2, 28, 'expense', 'Hogar',           'Artículos de limpieza',  15000,  'a1');
  // -3
  add(-3,  1, 'income',  'Sueldo',          'Sueldo del mes',         620000, 'a2');
  add(-3, 10, 'expense', 'Comida',          'Supermercado',           85000,  'a1');
  add(-3, 15, 'expense', 'Transporte',      'Uber + SUBE',            19000,  'a2');
  add(-3, 20, 'expense', 'Entretenimiento', 'Streaming + salida',     22000,  'a1');
  add(-3, 25, 'expense', 'Hogar',           'Reparaciones',           40000,  'a2');
  // -4
  add(-4,  1, 'income',  'Sueldo',          'Sueldo del mes',         600000, 'a2');
  add(-4, 10, 'expense', 'Comida',          'Supermercado',           90000,  'a1');
  add(-4, 15, 'expense', 'Transporte',      'Viaje',                  22000,  'a2');
  add(-4, 20, 'expense', 'Ropa',            'Ropa de temporada',      65000,  'a3');
  add(-4, 25, 'expense', 'Hogar',           'Muebles',                85000,  'a2');
  // -5
  add(-5,  1, 'income',  'Sueldo',          'Sueldo del mes',         600000, 'a2');
  add(-5,  5, 'expense', 'Comida',          'Supermercado',           88000,  'a1');
  add(-5, 15, 'expense', 'Entretenimiento', 'Salidas y regalos',      120000, 'a2');
  // -6
  add(-6,  1, 'income',  'Sueldo',          'Sueldo del mes',         580000, 'a2');
  add(-6, 10, 'expense', 'Comida',          'Supermercado',           82000,  'a1');
  add(-6, 20, 'expense', 'Transporte',      'Viaje de trabajo',       18000,  'a2');

  return {
    ...JSON.parse(JSON.stringify(EMPTY_DATA)),
    isDemo: true,
    accounts: [
      { id: 'a1', name: 'Efectivo',     type: 'cash',    balance: 45000,  currency: 'ARS', color: '#10B981' },
      { id: 'a2', name: 'Banco',        type: 'bank',    balance: 380000, currency: 'ARS', color: '#6366F1' },
      { id: 'a3', name: 'Billetera virtual', type: 'digital', balance: 127500, currency: 'ARS', color: '#818CF8' },
    ],
    debts: [
      { id: 'd1', name: 'Tarjeta Visa',      institution: 'Banco',         balance: 245000, originalAmount: 320000, rate: 24, minPayment: 18000, currency: 'ARS', color: '#F43F5E', dueDay: 15 },
      { id: 'd2', name: 'Préstamo Personal', institution: 'Financiera',    balance: 180000, originalAmount: 250000, rate: 45, minPayment: 15000, currency: 'ARS', color: '#F59E0B', dueDay: 22 },
      { id: 'd3', name: 'Cuotas celular',    institution: '12 cuotas s/i', balance: 72000,  originalAmount: 108000, rate: 0,  minPayment: 9000,  currency: 'ARS', color: '#A78BFA', dueDay: 5  },
    ],
    transactions: tx,
    budgets: [
      { id: 'b1', category: 'Comida',          limit: 130000, color: '#10B981' },
      { id: 'b2', category: 'Transporte',      limit: 40000,  color: '#F97316' },
      { id: 'b3', category: 'Servicios',       limit: 35000,  color: '#0EA5E9' },
      { id: 'b4', category: 'Entretenimiento', limit: 35000,  color: '#8B5CF6' },
      { id: 'b5', category: 'Salud',           limit: 25000,  color: '#EF4444' },
      { id: 'b6', category: 'Ropa',            limit: 40000,  color: '#EC4899' },
    ],
    goals: [
      { id: 'g1', name: 'Fondo de emergencia', iconName: 'shield', target: 500000, targetDate: null, createdAt: demoDate(-4, 1), contributions: [
        { id: 'gc1', date: demoDate(-4, 5),  amount: 40000, note: 'Arranque del fondo' },
        { id: 'gc2', date: demoDate(-3, 6),  amount: 30000 },
        { id: 'gc3', date: demoDate(-2, 5),  amount: 30000 },
        { id: 'gc4', date: demoDate(-1, 7),  amount: 27500 },
      ]},
      { id: 'g2', name: 'Vacaciones', iconName: 'plane', target: 300000, targetDate: demoDate(6, 15), createdAt: demoDate(-2, 1), contributions: [
        { id: 'gc5', date: demoDate(-2, 10), amount: 25000 },
        { id: 'gc6', date: demoDate(-1, 12), amount: 20000 },
      ]},
      { id: 'g3', name: 'Celular nuevo', iconName: 'smartphone', target: 250000, targetDate: null, createdAt: demoDate(-1, 1), contributions: [
        { id: 'gc7', date: demoDate(-1, 20), amount: 60000 },
      ]},
    ],
    settings: { name: 'Demo', currency: 'ARS', monthlyIncome: 735000, theme: 'light' },
    recurringIncomes: [
      { id: 'ri1', name: 'Sueldo',    amount: 670000, currency: 'ARS', category: 'Sueldo',    accountId: 'a2', dueDay: 1,  frequency: 'mensual', active: true, iconName: 'briefcase' },
      { id: 'ri2', name: 'Freelance', amount: 90000,  currency: 'ARS', category: 'Freelance', accountId: 'a3', dueDay: 10, frequency: 'mensual', active: true, iconName: 'code'      },
    ],
    fixedExpenses: [
      { id: 'f1',  name: 'Alquiler',         amount: 280000, currency: 'ARS', dueDay: 1,  category: 'Hogar',           active: true,  iconName: 'home',       frequency: 'mensual' },
      { id: 'f2',  name: 'Expensas',         amount: 45000,  currency: 'ARS', dueDay: 8,  category: 'Hogar',           active: true,  iconName: 'building',   frequency: 'mensual' },
      { id: 'f3',  name: 'Obra Social',      amount: 28000,  currency: 'ARS', dueDay: 1,  category: 'Salud',           active: true,  iconName: 'heart',      frequency: 'mensual' },
      { id: 'f4',  name: 'Internet',         amount: 18000,  currency: 'ARS', dueDay: 5,  category: 'Servicios',       active: true,  iconName: 'wifi',       frequency: 'mensual' },
      { id: 'f5',  name: 'Celular',          amount: 12500,  currency: 'ARS', dueDay: 10, category: 'Servicios',       active: true,  iconName: 'smartphone', frequency: 'mensual' },
      { id: 'f6',  name: 'ChatGPT Plus',     amount: 20,     currency: 'USD', dueDay: 8,  category: 'IA / Software',   active: true,  iconName: 'cpu',        frequency: 'mensual' },
      { id: 'f7',  name: 'Netflix',          amount: 7,      currency: 'USD', dueDay: 18, category: 'Entretenimiento', active: true,  iconName: 'film',       frequency: 'mensual' },
      { id: 'f8',  name: 'Spotify',          amount: 6,      currency: 'USD', dueDay: 22, category: 'Entretenimiento', active: true,  iconName: 'music',      frequency: 'mensual' },
      { id: 'f9',  name: 'Gimnasio',         amount: 25000,  currency: 'ARS', dueDay: 3,  category: 'Salud',           active: true,  iconName: 'heart',      frequency: 'mensual' },
      { id: 'f10', name: 'Clases de inglés', amount: 8000,   currency: 'ARS', dueDay: 1,  weekDay: 2, category: 'Educación', active: true, iconName: 'bookOpen', frequency: 'semanal' },
      { id: 'f11', name: 'Adobe CC',         amount: 55,     currency: 'USD', dueDay: 3,  category: 'IA / Software',   active: false, iconName: 'palette',    frequency: 'mensual' },
      { id: 'f12', name: 'Seguro auto',      amount: 180000, currency: 'ARS', dueDay: 15, annualMonth: 1, annualDay: 15, category: 'Servicios', active: true, iconName: 'car', frequency: 'anual' },
    ],
  };
}

// Compat: componentes legacy referencian DEFAULT_DATA — hoy es el esqueleto vacío
const DEFAULT_DATA = EMPTY_DATA;

const STORAGE_KEY = 'rumbo_data_v2';

// Devuelve los datos guardados normalizados, o null si es el primer arranque
// (null → App muestra la pantalla de bienvenida para elegir cero/demo).
function loadData() {
  let result = null;
  try {
    const saved = localStorage.getItem(STORAGE_KEY);
    if (saved) {
      const parsed = JSON.parse(saved);
      result = {
        ...JSON.parse(JSON.stringify(EMPTY_DATA)),
        ...parsed,
        settings: { ...EMPTY_DATA.settings, ...parsed.settings },
        fxRates:  { ...EMPTY_DATA.fxRates,  ...parsed.fxRates  },
        customCategories: parsed.customCategories ?? [],
      };
    }
  } catch(e) {}
  if (!result) return null;
  // Migración: metas viejas con `current` suelto → historial de aportes
  result.goals = (result.goals || []).map(g => {
    if (g.contributions) return g;
    const initial = Number(g.current) || 0;
    return {
      ...g,
      iconName: g.iconName || 'piggyBank',
      targetDate: g.targetDate || null,
      contributions: initial > 0
        ? [{ id: g.id + '-init', date: `${CURRENT_MONTH}-01`, amount: initial, note: 'Saldo inicial' }]
        : [],
    };
  });
  // Registrar custom categories en el CATEGORIES global para que CategoryIcon las encuentre
  (result.customCategories || []).forEach(cc => {
    if (!CATEGORIES.find(c => c.name === cc.name)) CATEGORIES.push(cc);
  });
  return result;
}

// Total aportado a una meta de ahorro
const goalSaved = (g) => (g.contributions || []).reduce((s, c) => s + (Number(c.amount) || 0), 0);

/* Estado del plan de una meta con cadencia elegida (planCadence: 'day'|'week'|'month').
   Devuelve la cuota vigente (siempre sobre lo que FALTA) y si el período actual ya está cubierto. */
function goalPlanStatus(goal) {
  if (!goal || !goal.planCadence) return null;
  const saved = goalSaved(goal);
  const remaining = Math.max(0, (goal.target || 0) - saved);
  if (remaining <= 0) return { done: true };
  const months = (() => {
    if (goal.targetDate) {
      const now = new Date();
      const t = new Date(goal.targetDate + 'T00:00:00');
      return Math.max(1, (t.getFullYear() - now.getFullYear()) * 12 + (t.getMonth() - now.getMonth()));
    }
    return goal.planMonths || 6;
  })();
  const days = Math.max(1, Math.round(months * 30.44));
  const amount = goal.planCadence === 'day' ? Math.round(remaining / days)
    : goal.planCadence === 'week' ? Math.round((remaining / days) * 7)
    : Math.round(remaining / months);
  const now = new Date();
  const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
  const todayISO = iso(now);
  const weekStart = (() => { const d = new Date(now); d.setDate(d.getDate() - ((d.getDay() + 6) % 7)); return iso(d); })();
  const contribs = goal.contributions || [];
  const covered = goal.planCadence === 'day' ? contribs.some(c => c.date === todayISO)
    : goal.planCadence === 'week' ? contribs.some(c => (c.date || '') >= weekStart && (c.date || '') <= todayISO)
    : contribs.some(c => (c.date || '').startsWith(todayISO.slice(0, 7)));
  const label  = goal.planCadence === 'day' ? 'por día' : goal.planCadence === 'week' ? 'por semana' : 'por mes';
  const period = goal.planCadence === 'day' ? 'Hoy' : goal.planCadence === 'week' ? 'Esta semana' : 'Este mes';
  return { done: false, amount, covered, label, period };
}

// Datos limpios para "Empezar de cero" (con nombre opcional)
function freshData(name) {
  const d = JSON.parse(JSON.stringify(EMPTY_DATA));
  if (name) d.settings.name = name.trim();
  return d;
}

// Registra una categoria custom en el CATEGORIES global (para CategoryIcon y getCategoryMeta)
function registerCustomCategory(cc) {
  if (!CATEGORIES.find(c => c.name === cc.name)) CATEGORIES.push(cc);
}

function saveData(data) {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch(e) {}
}

// Paleta sutil: mismos hues que antes (familiar) pero saturación bajada para reducir ruido visual.
// Filosofía: que se reconozca la categoría sin que el ojo "salte" entre tags. Estilo Linear / Notion.
const CATEGORIES = [
  { name: 'Sueldo',         type: 'income',  color: '#0F8A6A', bg: '#E8F5F0', iconName: 'briefcase' },
  { name: 'Freelance',      type: 'income',  color: '#4F46E5', bg: '#E8E9FB', iconName: 'code'      },
  { name: 'Inversiones',    type: 'income',  color: '#5B5FD3', bg: '#EBECF8', iconName: 'trending'  },
  { name: 'Otros ingresos', type: 'income',  color: '#7C5BC9', bg: '#EFEBF7', iconName: 'gift'      },
  { name: 'Comida',         type: 'expense', color: '#0F8A6A', bg: '#E8F5F0', iconName: 'shoppingCart' },
  { name: 'Transporte',     type: 'expense', color: '#C2410C', bg: '#FBEBDD', iconName: 'car'       },
  { name: 'Servicios',      type: 'expense', color: '#0369A1', bg: '#E0F0F8', iconName: 'zap'       },
  { name: 'Entretenimiento',type: 'expense', color: '#6D28D9', bg: '#EBE5F7', iconName: 'film'      },
  { name: 'Salud',          type: 'expense', color: '#B91C1C', bg: '#F8E5E5', iconName: 'heart'     },
  { name: 'Ropa',           type: 'expense', color: '#9D174D', bg: '#F4E4ED', iconName: 'shirt'     },
  { name: 'Deudas',         type: 'expense', color: '#9F1239', bg: '#F4E0E6', iconName: 'creditCard'},
  { name: 'Educación',      type: 'expense', color: '#1D4ED8', bg: '#E1E8F8', iconName: 'bookOpen'  },
  { name: 'Hogar',          type: 'expense', color: '#B45309', bg: '#F8EDD7', iconName: 'home'      },
  { name: 'Otros gastos',   type: 'expense', color: '#64748B', bg: '#EDF1F5', iconName: 'package'   },
];

// Resuelve símbolo según moneda (default ARS). Robusto si SUPPORTED_CURRENCIES no está cargado todavía.
const _curSym = (currency) => {
  if (!currency) return '$';
  if (typeof SUPPORTED_CURRENCIES === 'undefined') return '$';
  const cur = SUPPORTED_CURRENCIES.find(c => c.code === currency);
  return cur ? cur.symbol : currency;
};

const fmt = (n, currency) => {
  const sym = _curSym(currency);
  const absN = Math.abs(n);
  if (absN >= 1000000) return `${sym}${(n/1000000).toFixed(1)}M`;
  if (absN >= 1000)    return `${sym}${(n/1000).toFixed(0)}K`;
  return `${sym}${n.toLocaleString('es-AR')}`;
};

// fmtFull: preserva decimales si hay (no redondea). Acepta moneda opcional para el símbolo.
const fmtFull = (n, currency) => {
  const sym = _curSym(currency);
  const num = Number(n) || 0;
  const hasDecimals = Math.abs(num % 1) > 0.0001;
  return `${sym}${num.toLocaleString('es-AR', {
    minimumFractionDigits: hasDecimals ? 2 : 0,
    maximumFractionDigits: 2,
  })}`;
};

const getCategoryMeta     = (cat) => CATEGORIES.find(c => c.name === cat) || { color: '#94A3B8', bg: '#F1F5F9', iconName: 'package' };
const getCategoryColor    = (cat) => getCategoryMeta(cat).color;
const getCategoryBg       = (cat) => getCategoryMeta(cat).bg;
const getCategoryIconName = (cat) => getCategoryMeta(cat).iconName;
// Backward-compat (algunos componentes legacy aún usan getCategoryEmoji)
const getCategoryEmoji    = (cat) => getCategoryMeta(cat).iconName;

/* ── CategoryIcon — fondo neutro adaptable al tema, icono en color de la categoría ── */
const CategoryIcon = ({ category, size = 'md', style = {} }) => {
  const meta = getCategoryMeta(category);
  const dims = {
    xs: { box: 26, icon: 13, radius: 8  },
    sm: { box: 32, icon: 15, radius: 10 },
    md: { box: 42, icon: 19, radius: 13 },
    lg: { box: 52, icon: 23, radius: 16 },
  }[size] || { box: 42, icon: 19, radius: 13 };

  return (
    <div style={{
      width: dims.box, height: dims.box,
      borderRadius: dims.radius,
      background: 'var(--surface-2)',
      border: '1px solid var(--border)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      flexShrink: 0,
      ...style,
    }}>
      <Icon name={meta.iconName} size={dims.icon} color={meta.color} strokeWidth={1.8} />
    </div>
  );
};

// Format date label relative to today
function fmtDateLabel(dateStr) {
  const today   = new Date(); today.setHours(0,0,0,0);
  const d       = new Date(dateStr + 'T00:00:00');
  const diff    = Math.round((today - d) / 86400000);
  if (diff === 0) return 'Hoy';
  if (diff === 1) return 'Ayer';
  return d.toLocaleDateString('es-AR', { day: 'numeric', month: 'long' });
}

// Get month label
function getMonthLabel(yyyymm) {
  const [y, m] = yyyymm.split('-').map(Number);
  return new Date(y, m-1, 1).toLocaleDateString('es-AR', { month: 'long', year: 'numeric' });
}

// Helpers de navegación temporal — devuelven YYYY-MM
function shiftMonth(yyyymm, delta) {
  const [y, m] = yyyymm.split('-').map(Number);
  const d = new Date(y, m - 1 + delta, 1);
  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
}
const getPrevMonth = (yyyymm) => shiftMonth(yyyymm, -1);
const getNextMonth = (yyyymm) => shiftMonth(yyyymm, +1);

/* ─────────────────────────────────────────────────────────────
   Proyección de un mes — la pieza final del "rumbo"
   Combina: (a) lo registrado en transactions
            (b) ingresos recurrentes esperados (recurringIncomes)
            (c) gastos fijos esperados (fixedExpenses)
            (d) cuotas mínimas de deudas
   Para mes pasado/actual: usa lo registrado + lo que faltó por venir del mes
   Para mes futuro: solo proyección (no hay transactions).
───────────────────────────────────────────────────────────── */
function getMonthProjection(data, yyyymm) {
  const todayKey = `${new Date().getFullYear()}-${String(new Date().getMonth()+1).padStart(2,'0')}`;
  const isPast    = yyyymm < todayKey;
  const isCurrent = yyyymm === todayKey;
  const isFuture  = yyyymm > todayKey;
  const today     = new Date(); today.setHours(0,0,0,0);

  const txs = (data.transactions || []).filter(t => (t.date||'').startsWith(yyyymm));
  const realIncome  = txs.filter(t => t.type === 'income').reduce((s,t) => s+t.amount, 0);
  const realExpense = txs.filter(t => t.type === 'expense').reduce((s,t) => s+t.amount, 0);

  // Items recurrentes proyectados
  const projectedIncomes = [];
  (data.recurringIncomes || []).filter(r => r.active).forEach(r => {
    getDueDatesInMonth(r, yyyymm).forEach(date => {
      // Si es mes pasado o actual y la fecha ya pasó, asumimos que está reflejada en transactions
      // → no la sumamos para no duplicar
      if (!isFuture && date <= today) return;
      projectedIncomes.push({ source:'recurringIncome', item:r, date, amount: r.amount, currency: r.currency });
    });
  });

  const projectedExpenses = [];
  (data.fixedExpenses || []).filter(f => f.active).forEach(f => {
    getDueDatesInMonth(f, yyyymm).forEach(date => {
      if (!isFuture && date <= today) return;
      projectedExpenses.push({ source:'fixed', item:f, date, amount: f.amount, currency: f.currency });
    });
  });
  (data.debts || []).forEach(d => {
    getDueDatesInMonth({ ...d, frequency: d.frequency || 'mensual' }, yyyymm).forEach(date => {
      if (!isFuture && date <= today) return;
      projectedExpenses.push({ source:'debt', item:d, date, amount: d.minPayment || 0, currency: d.currency });
    });
  });

  // Convertir a moneda base para sumar
  const baseCurrency = data.settings?.currency || 'ARS';
  const fxRates = data.fxRates || {};
  const toBase = (amount, cur) => (typeof convertToBase === 'function')
    ? convertToBase(amount, cur, fxRates, baseCurrency)
    : amount;
  const projIncBase = projectedIncomes.reduce((s,i) => s + toBase(i.amount, i.currency), 0);
  const projExpBase = projectedExpenses.reduce((s,i) => s + toBase(i.amount, i.currency), 0);

  return {
    yyyymm,
    isPast, isCurrent, isFuture,
    real:      { income: realIncome, expense: realExpense, net: realIncome - realExpense },
    projected: { income: projIncBase, expense: projExpBase, net: projIncBase - projExpBase, items: { incomes: projectedIncomes, expenses: projectedExpenses } },
    expected:  {
      income:  realIncome  + projIncBase,
      expense: realExpense + projExpBase,
      net:     (realIncome  + projIncBase) - (realExpense + projExpBase),
    },
  };
}

/* Devuelve el saldo retroactivo de una cuenta al CIERRE de un mes dado
   (último día del mes a las 23:59). Para meses futuros devuelve null porque
   no se puede calcular retroactivamente sin proyección. */
function getAccountBalanceAtMonthEnd(account, transactions, yyyymm) {
  const todayKey = `${new Date().getFullYear()}-${String(new Date().getMonth()+1).padStart(2,'0')}`;
  if (yyyymm > todayKey) return null;
  // Cutoff: primer día del mes siguiente. Tx >= cutoff son "posteriores al cierre".
  const cutoff = shiftMonth(yyyymm, 1) + '-01';
  let balance = account.balance;
  transactions.forEach(t => {
    if (t.accountId !== account.id) return;
    if (t.date < cutoff) return;
    if (t.type === 'income')  balance -= t.amount;
    else if (t.type === 'expense') balance += t.amount;
  });
  return balance;
}

/* ─────────────────────────────────────────────────────────────
   Calcula las fechas de vencimiento de un ítem recurrente
   (gasto fijo o cuota de deuda) dentro de un mes específico.
   Item shape: { dueDay, frequency, weekDay?, annualMonth?, annualDay? }
   Devuelve array de fechas Date ordenadas.
───────────────────────────────────────────────────────────── */
function getDueDatesInMonth(item, yyyymm) {
  const [y, m] = yyyymm.split('-').map(Number);
  const lastDay = new Date(y, m, 0).getDate(); // último día del mes
  const freq = item.frequency || 'mensual';
  const dates = [];

  if (freq === 'mensual') {
    const day = Math.min(parseInt(item.dueDay) || 1, lastDay);
    dates.push(new Date(y, m-1, day));
  } else if (freq === 'quincenal') {
    const start = Math.min(parseInt(item.dueDay) || 1, lastDay);
    dates.push(new Date(y, m-1, start));
    const second = start + 15;
    if (second <= lastDay) dates.push(new Date(y, m-1, second));
  } else if (freq === 'semanal') {
    const targetDow = parseInt(item.weekDay) || 0; // 0=Lun … 6=Dom
    // Convertir nuestro 0=Lun a JS 1=Lun (donde 0=Dom)
    const jsDow = (targetDow + 1) % 7;
    for (let d = 1; d <= lastDay; d++) {
      if (new Date(y, m-1, d).getDay() === jsDow) dates.push(new Date(y, m-1, d));
    }
  } else if (freq === 'diario') {
    for (let d = 1; d <= lastDay; d++) dates.push(new Date(y, m-1, d));
  } else if (freq === 'anual') {
    const am = parseInt(item.annualMonth) || 1;
    if (am === m) {
      const day = Math.min(parseInt(item.annualDay) || 1, lastDay);
      dates.push(new Date(y, m-1, day));
    }
  }
  return dates;
}

Object.assign(window, {
  loadData, saveData, DEFAULT_DATA, EMPTY_DATA, buildDemoData, freshData, goalSaved, goalPlanStatus, CATEGORIES, registerCustomCategory,
  CURRENT_MONTH, PREV_MONTH,
  fmt, fmtFull,
  getCategoryMeta, getCategoryColor, getCategoryBg, getCategoryIconName, getCategoryEmoji,
  CategoryIcon,
  fmtDateLabel, getMonthLabel,
  shiftMonth, getPrevMonth, getNextMonth, getDueDatesInMonth, getAccountBalanceAtMonthEnd, getMonthProjection,
  STORAGE_KEY,
});
