/* === Initial state, formatters, and date helpers === */

const TAG_COLORS = {
  terracotta: '#e2552a',
  ochre:      '#d99319',
  olive:      '#9aa824',
  moss:       '#5fa83a',
  teal:       '#1ea69e',
  slate:      '#4f74d6',
  plum:       '#a64ad9',
  rose:       '#e64a7a',
  ink:        '#3a3228',
  sand:       '#c89858',
};

const SEED_TAGS = [
  { id: 'food',     name: 'Groceries',     color: 'moss',       budget: 400 },
  { id: 'dining',   name: 'Dining',        color: 'terracotta', budget: 220 },
  { id: 'transit',  name: 'Transit',       color: 'slate',      budget: 120 },
  { id: 'fuel',     name: 'Fuel',          color: 'ink',        budget: 140 },
  { id: 'home',     name: 'Home',          color: 'sand',       budget: 150 },
  { id: 'health',   name: 'Health',        color: 'plum',       budget:  80 },
  { id: 'fun',      name: 'Entertainment', color: 'ochre',      budget: 120 },
  { id: 'gift',     name: 'Gifts',         color: 'rose',       budget:  60 },
  { id: 'subs',     name: 'Subscriptions', color: 'teal',       budget:  40 },
  { id: 'work',     name: 'Work',          color: 'olive',      budget:   0 },
];

/* Tags ship with reasonable colors but no budgets — edit them on the Tags screen. */
const STARTER_TAGS = SEED_TAGS.map(t => ({ ...t, budget: 0 }));

/* Family starts with a single primary member you can rename on the Family screen. */
const STARTER_FAMILY = [
  { id: 'self', name: 'Me', initials: 'ME', color: 'slate', budget: 0, role: 'primary' },
];

const INITIAL_STATE = {
  income: 0,
  incomeLabel: '',
  fixed: [],
  expenses: [],
  extra: [],
  tags: STARTER_TAGS,
  family: STARTER_FAMILY,
  goals: [],
  history: [],
  /* Single liquid pool of unallocated cash. Treated as your "savings buffer". */
  pool: 0,
  /* Manual adjustments to the pool — direct edits AND month close-outs both record an entry here.
     { id, date, amount (signed), kind: 'closeout'|'adjust'|'goal-deposit'|'goal-withdraw', monthKey?, note? } */
  poolLedger: [],
  /* Months that have been closed out — prevents double-commit. */
  closedMonths: [],
  /* Tasks with optional subtasks and family-member assignments.
     { id, title, dueDate?: 'YYYY-MM-DD', memberId?, groupId?, note?, done,
       recurrence?: { type: 'daily'|'weekly'|'monthly' }   // anchored to dueDate
       subtasks: [{ id, title, memberId?, done }] } */
  tasks: [],
  /* Task groups — bucket tasks under labels like "Home", "Car", "Finance".
     { id, name, color: keyof TAG_COLORS } */
  taskGroups: [],
  /* Meal-planner recipes.
     { id, name, mealTags: [], portions, leftovers, ingredients: [{ name, qty, unit, estCost }], note } */
  recipes: [],
  /* Planned meals for specific dates.
     { id, date: 'YYYY-MM-DD', mealType: 'breakfast'|'lunch'|'dinner', recipeId } */
  mealPlan: [],
  /* Pantry staples — names that get filtered out of generated shopping lists.
     { id, name, lowOnStock?, category? } */
  pantry: [],
};

// Pantry categories — fixed set with friendly labels. Adding a new one here
// extends the picker, the section list, and the filter pills automatically.
const PANTRY_CATEGORIES = [
  { id: 'produce',     label: 'Produce' },
  { id: 'fridge',      label: 'Fridge' },
  { id: 'freezer',     label: 'Freezer' },
  { id: 'dry-goods',   label: 'Dry goods' },
  { id: 'spices',      label: 'Spices' },
  { id: 'herbs',       label: 'Herbs' },
  { id: 'oils',        label: 'Oils & vinegars' },
  { id: 'condiments',  label: 'Condiments' },
  { id: 'baking',      label: 'Baking' },
  { id: 'bread',       label: 'Bread' },
  { id: 'snacks',      label: 'Snacks' },
  { id: 'other',       label: 'Other' },
];
const PANTRY_CATEGORY_LABELS = Object.fromEntries(PANTRY_CATEGORIES.map(c => [c.id, c.label]));

// Helpers
const fmt = (n, opts = {}) => {
  const { sign = false, cents = true } = opts;
  const abs = Math.abs(n);
  const formatted = abs.toLocaleString('en-US', {
    minimumFractionDigits: cents ? 2 : 0,
    maximumFractionDigits: cents ? 2 : 0,
  });
  const s = n < 0 ? '−' : (sign ? '+' : '');
  return s + '$' + formatted;
};

const fmtCompact = (n) => {
  const abs = Math.abs(n);
  if (abs >= 1000) return '$' + (n / 1000).toFixed(abs >= 10000 ? 0 : 1) + 'k';
  return '$' + Math.round(n);
};

const tagById = (tags, id) => tags.find(t => t.id === id) || { name: 'Untagged', color: 'slate' };
const memberById = (members, id) => members.find(m => m.id === id) || { name: 'Unassigned', color: 'slate', initials: '?' };

// First + last letter of a name, uppercased. "Me" → "ME", "Sarah" → "SH", "Megan" → "MN".
const nameInitials = (name) => {
  const s = (name || '').trim();
  if (!s) return '';
  return s[0].toUpperCase();
};

// Normalize a fixed cost's amount to its monthly equivalent so weekly/yearly bills
// can be summed alongside monthly ones.
const monthlyEquiv = (f) => {
  const freq = f.frequency || 'monthly';
  if (freq === 'weekly') return f.amount * 52 / 12;
  if (freq === 'yearly') return f.amount / 12;
  return f.amount;
};

const monthDay = (iso) => {
  const d = new Date(iso + 'T00:00:00');
  const m = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][d.getMonth()];
  return `${m} ${d.getDate()}`;
};

const daysUntil = (iso) => {
  const d = new Date(iso + 'T00:00:00');
  const now = new Date(TODAY.getFullYear(), TODAY.getMonth(), TODAY.getDate());
  return Math.ceil((d - now) / (1000 * 60 * 60 * 24));
};

const monthsUntil = (iso) => {
  const d = new Date(iso + 'T00:00:00');
  const now = new Date(TODAY.getFullYear(), TODAY.getMonth(), TODAY.getDate());
  return (d.getFullYear() - now.getFullYear()) * 12 + (d.getMonth() - now.getMonth()) + (d.getDate() >= now.getDate() ? 0 : -1) + 1;
};

// Month helpers — live "now" (was hardcoded to 2026-04-25 during prototyping)
const TODAY = new Date();
const MONTH_NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const MONTH_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];

const monthKey = (iso) => iso.slice(0, 7); // 'YYYY-MM'
const currentMonthKey = () => `${TODAY.getFullYear()}-${String(TODAY.getMonth()+1).padStart(2,'0')}`;
const formatMonthKey = (key) => {
  const [y, m] = key.split('-');
  return `${MONTH_NAMES[parseInt(m)-1]} ${y}`;
};
const formatMonthKeyShort = (key) => {
  const [y, m] = key.split('-');
  return `${MONTH_SHORT[parseInt(m)-1]} ${y.slice(2)}`;
};
const shiftMonth = (key, delta) => {
  const [y, m] = key.split('-').map(Number);
  const d = new Date(y, m - 1 + delta, 1);
  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
};
const isCurrentMonth = (key) => key === currentMonthKey();
const isFutureMonth = (key) => key > currentMonthKey();

// Days in a given month key
const daysInMonthKey = (key) => {
  const [y, m] = key.split('-').map(Number);
  return new Date(y, m, 0).getDate();
};

// === Meal-planner helpers ===

const MEAL_TYPES = ['breakfast', 'lunch', 'dinner'];
const MEAL_TYPE_LABELS = { breakfast: 'Breakfast', lunch: 'Lunch', dinner: 'Dinner' };
const COST_TIERS = ['I', 'II', 'III'];
const COST_TIER_NAMES = ['economy', 'standard', 'premium'];
const COST_TIER_THRESHOLDS = [10, 25];

const costTierIndex = (n) => {
  if (n < COST_TIER_THRESHOLDS[0]) return 0;
  if (n < COST_TIER_THRESHOLDS[1]) return 1;
  return 2;
};

// Recipes are evergreen — `recipeCost` is the full cost-to-cook regardless of what's
// currently in your pantry. The pantry is the single source of truth for "what I have"
// at any given moment, so pantry-aware pricing is computed separately at read time.
const recipeCost = (r) => (r?.ingredients || []).reduce(
  (s, i) => s + (parseFloat(i.estCost) || 0),
  0
);

// Set of pantry ingredient names that are currently in stock (i.e., not flagged low/out).
// Centralised so the shopping list and recipe-cost views agree on the same key shape.
const pantryInStockSet = (pantry) => new Set(
  (pantry || [])
    .filter(p => !p.lowOnStock)
    .map(p => (p.name || '').toLowerCase().trim())
    .filter(Boolean)
);

// Recipe cost adjusted for what's currently in your pantry: ingredients whose names
// match a pantry item that's not flagged low are treated as $0 for *this* moment.
// Recipe data itself is untouched — flip a pantry flag and every recipe re-prices.
const recipeCostWithPantry = (r, pantry) => {
  const inStock = pantryInStockSet(pantry);
  return (r?.ingredients || []).reduce((s, i) => {
    const key = (i.name || '').toLowerCase().trim();
    return inStock.has(key) ? s : s + (parseFloat(i.estCost) || 0);
  }, 0);
};

const isoDate = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;

// Monday-anchored week start
const weekStartIso = (iso) => {
  const d = new Date(iso + 'T00:00:00');
  const day = (d.getDay() + 6) % 7; // 0 = Mon
  d.setDate(d.getDate() - day);
  return isoDate(d);
};

const addDaysIso = (iso, n) => {
  const d = new Date(iso + 'T00:00:00');
  d.setDate(d.getDate() + n);
  return isoDate(d);
};

const weekDates = (startIso) => Array.from({ length: 7 }, (_, i) => addDaysIso(startIso, i));

const weekdayShort = (iso) => {
  const d = new Date(iso + 'T00:00:00');
  return ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'][(d.getDay() + 6) % 7];
};

const weekdayLong = (iso) => {
  const d = new Date(iso + 'T00:00:00');
  return ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'][(d.getDay() + 6) % 7];
};

const formatWeekRange = (startIso) => {
  const start = new Date(startIso + 'T00:00:00');
  const end = new Date(addDaysIso(startIso, 6) + 'T00:00:00');
  const sm = MONTH_SHORT[start.getMonth()], em = MONTH_SHORT[end.getMonth()];
  if (start.getMonth() === end.getMonth()) return `${sm} ${start.getDate()}–${end.getDate()}`;
  return `${sm} ${start.getDate()} – ${em} ${end.getDate()}`;
};

// Build a deduped shopping list from a set of planned meals, minus in-stock pantry staples.
//
// Recipes are evergreen — the pantry is the only thing that determines what's skipped.
// An ingredient drops off the list when its name matches a pantry item that's not flagged low.
//
// Per-ingredient flags honored:
//   purchaseCost     — what you actually pay at the store (vs. estCost = portion used in recipe)
//   leftoverToPantry — when true, marks the line so checking it off later can stock the pantry
//
// Pantry items flagged `lowOnStock` are NOT filtered out and are also added as standalone entries
// so they show up even when no planned recipe references them.
const buildShoppingList = (plan, recipes, pantry) => {
  const inStock = pantryInStockSet(pantry);
  const map = new Map();
  (plan || []).forEach(p => {
    const r = recipes.find(rr => rr.id === p.recipeId);
    if (!r) return;
    (r.ingredients || []).forEach(ing => {
      const name = (ing.name || '').trim();
      if (!name) return;
      const key = name.toLowerCase();
      if (inStock.has(key)) return;             // pantry shadow — present and not flagged low
      const existing = map.get(key) || {
        name, qty: 0, unit: ing.unit || '',
        estCost: 0, hasQty: false,
        recipes: new Set(),
        fromPantry: false,
        leftoverToPantry: false,
      };
      const qty = parseFloat(ing.qty);
      if (!isNaN(qty)) { existing.qty += qty; existing.hasQty = true; }
      // Shopping line uses bulk cost when set, otherwise the recipe-portion cost.
      const lineCost = parseFloat(ing.purchaseCost) || parseFloat(ing.estCost) || 0;
      existing.estCost += lineCost;
      existing.recipes.add(r.name);
      if (!existing.unit && ing.unit) existing.unit = ing.unit;
      if (ing.leftoverToPantry) existing.leftoverToPantry = true;
      map.set(key, existing);
    });
  });
  // Pantry items flagged low/out: ensure each appears at least once.
  (pantry || []).filter(p => p.lowOnStock).forEach(p => {
    const name = (p.name || '').trim();
    if (!name) return;
    const key = name.toLowerCase();
    const existing = map.get(key);
    if (existing) { existing.fromPantry = true; return; }
    map.set(key, {
      name, qty: 0, unit: '', estCost: 0,
      recipes: new Set(['Pantry restock']),
      hasQty: false, fromPantry: true, leftoverToPantry: false,
    });
  });
  return [...map.values()].map(v => ({ ...v, recipes: [...v.recipes] }));
};

/* Advance a recurring task's due date — steps the schedule forward at least
   once, then keeps stepping while still <= today (catches up if the task was
   left untouched across many cycles). Anchor day-of-month is preserved across
   short months (Jan 31 → Feb 28 → Mar 31). `interval` is the spacing (every
   N days/weeks/months). */
const advanceRecurrence = (iso, type, interval = 1) => {
  if (!iso || !type) return iso;
  const n = Math.max(1, parseInt(interval, 10) || 1);
  const todayIso = isoDate(TODAY);
  const parse = (s) => {
    const [y, m, d] = s.split('-').map(Number);
    return new Date(y, m - 1, d);
  };
  const fmtIso = (d) =>
    d.getFullYear() + '-' +
    String(d.getMonth() + 1).padStart(2, '0') + '-' +
    String(d.getDate()).padStart(2, '0');
  const targetDay = parse(iso).getDate();

  const step = (d) => {
    if (type === 'daily')   { d.setDate(d.getDate() + n); return d; }
    if (type === 'weekly')  { d.setDate(d.getDate() + 7 * n); return d; }
    if (type === 'monthly') {
      const next = new Date(d.getFullYear(), d.getMonth() + n, 1);
      const lastDay = new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate();
      next.setDate(Math.min(targetDay, lastDay));
      return next;
    }
    return d;
  };

  if (!['daily','weekly','monthly'].includes(type)) return iso;

  let d = step(parse(iso));
  let i = 0;
  while (fmtIso(d) <= todayIso && i++ < 600) d = step(d);
  return fmtIso(d);
};

const recurrenceLabel = (type, interval = 1) => {
  const n = Math.max(1, parseInt(interval, 10) || 1);
  const unit = type === 'daily' ? 'day' : type === 'weekly' ? 'week' : type === 'monthly' ? 'month' : null;
  if (!unit) return null;
  if (n === 1) return type === 'daily' ? 'Daily' : type === 'weekly' ? 'Weekly' : 'Monthly';
  return `Every ${n} ${unit}s`;
};

Object.assign(window, {
  TAG_COLORS, INITIAL_STATE, SEED_TAGS,
  fmt, fmtCompact, tagById, memberById, nameInitials, monthlyEquiv, monthDay, daysUntil, monthsUntil,
  TODAY, MONTH_NAMES, MONTH_SHORT,
  monthKey, currentMonthKey, formatMonthKey, formatMonthKeyShort,
  shiftMonth, isCurrentMonth, isFutureMonth, daysInMonthKey,
  MEAL_TYPES, MEAL_TYPE_LABELS, COST_TIERS, COST_TIER_NAMES, COST_TIER_THRESHOLDS,
  PANTRY_CATEGORIES, PANTRY_CATEGORY_LABELS,
  costTierIndex, recipeCost, recipeCostWithPantry, pantryInStockSet,
  isoDate, weekStartIso, addDaysIso, weekDates, weekdayShort, weekdayLong,
  formatWeekRange, buildShoppingList,
  advanceRecurrence, recurrenceLabel,
});
