// Store — single source of truth for the planning state.
// Wraps everything in <StoreProvider/>; screens read via useStore / useWedding / useGuests / useTasks.
// Persists to localStorage. Designed so a CloudflareSyncAdapter can be swapped in later
// without touching screens.

const STORE_KEY = 'weddingos.v1';
const SCHEMA_VERSION = 11;

const StoreContext = React.createContext(null);

// Convert a demo RSVP.guests row into the canonical guest record used by the store.
function normalizeGuest(g, i) {
  let partySize = 1;
  // "Name & Other" implies a 2-person household.
  if (/\s&\s/.test(g.name)) partySize = 2;
  // Plus-one adds one more.
  if (g.plus) partySize += 1;
  // "(N)" suffix overrides — e.g. "Theo's groomsmen (4)" = 4 people.
  const groomsmen = /\((\d+)\)/.exec(g.name);
  if (groomsmen) partySize = parseInt(groomsmen[1], 10);
  return {
    id: g.id || `g${i}-${Date.now().toString(36)}`,
    name: g.name,
    group: g.group || '',
    status: g.status || 'pending',
    meal: g.meal || '',
    dietary: g.note || g.dietary || '',
    plus: !!g.plus,
    plusName: g.plusName || '',
    partySize,
    table: g.table || null,
  };
}

// An empty starting state for a real user filling things in from scratch.
function makeEmptyState() {
  return {
    schemaVersion: SCHEMA_VERSION,
    wedding: {
      brides:     '',
      groom:      '',
      dateISO:    '',
      city:       '',
      cityShort:  '',
      venue:      '',
      timeLabel:  '',
      guestCount: 0,
      budget:     0,
    },
    tasks: [],
    guests: [],
    budgetCategories: [],
    vendors: [],
    tables: [],
    events: [],
    party: [],
    dayOfTimeline: [],
    emergencyContacts: [],
    moodboard: {
      vibe: '',
      story: '',
      palette: [],
      inspiration: [],
    },
    updatedAt: Date.now(),
  };
}

function makeDefaultState() {
  return {
    schemaVersion: SCHEMA_VERSION,
    wedding: {
      brides:     WEDDING.brides,
      groom:      WEDDING.groom,
      dateISO:    WEDDING.date.toISOString(),
      city:       WEDDING.city,
      cityShort:  'Mendocino, CA',
      venue:      'Hollow Tree',
      timeLabel:  '4:00 PM',
      guestCount: WEDDING.guestCount,
      budget:     WEDDING.budget,
    },
    tasks: TASKS.map((t, i) => ({
      ...t,
      status: 'assigned',  // 'assigned' | 'inProgress' | 'done'
      notes: '',
      dismissed: false,
      assignedTo: ['sloane', 'theo', 'shared', 'sloane', 'theo'][i % 5],
    })),
    guests: RSVP.guests.map(normalizeGuest),
    budgetCategories: BUDGET.categories.map((c, i) => ({
      id: `bc${i}-${Date.now().toString(36)}`,
      name:    c.name,
      planned: c.planned || 0,
      spent:   c.spent   || 0,
      icon:    c.icon    || 'sparkle',
      note:    c.note    || '',
    })),
    vendors: VENDORS.list.map((v, i) => ({
      id: `v${i}-${Date.now().toString(36)}`,
      name:    v.name,
      role:    v.role    || '',
      contact: v.contact || '',
      status:  v.status  || 'researching',
      paid:    v.paid    || 0,
      total:   v.total   || 0,
      next:    v.next    || '',
      notes:   v.notes   || '',
    })),
    tables: SEATING.tables.map((t, i) => ({
      id: `t${i}-${Date.now().toString(36)}`,
      number:   t.id,
      name:     t.name,
      kind:     t.kind || 'friends',
      capacity: t.capacity || 8,
      seats:    [...(t.seats || [])],
    })),
    events: (() => {
      const out = [];
      Object.entries(CALENDAR.events || {}).forEach(([day, list]) => {
        const dayN = parseInt(day, 10);
        const dateISO = new Date(2026, 8, dayN, 12, 0, 0).toISOString();
        list.forEach((e, j) => out.push({
          id: `e${dayN}-${j}-${Date.now().toString(36)}`,
          dateISO,
          title: e.title,
          time:  e.time || '',
          kind:  e.kind || 'event',
          note:  e.note || '',
        }));
      });
      return out;
    })(),
    party: (() => {
      const flat = [];
      (PARTY.bridal || []).forEach((p, i) => flat.push({
        id: `pb${i}-${Date.now().toString(36)}`,
        section: 'bridal', side: p.side || 'sloane',
        name: p.name, role: p.role,
      }));
      (PARTY.groom  || []).forEach((p, i) => flat.push({
        id: `pg${i}-${Date.now().toString(36)}`,
        section: 'groom',  side: p.side || 'theo',
        name: p.name, role: p.role,
      }));
      (PARTY.family || []).forEach((p, i) => flat.push({
        id: `pf${i}-${Date.now().toString(36)}`,
        section: 'family', side: p.side || 'sloane',
        name: p.name, role: p.role,
      }));
      [PARTY.officiant, PARTY.ringbearer, PARTY.flowergirl]
        .filter(Boolean)
        .forEach((p, i) => flat.push({
          id: `ps${i}-${Date.now().toString(36)}`,
          section: 'special', side: 'both',
          name: p.name, role: p.role,
        }));
      return flat;
    })(),
    dayOfTimeline: [
      { id: 'do1',  time: '7:00 AM',  title: 'Hair & makeup begins',     where: 'Bridal suite · Hollow Tree', icon: 'sparkle', kind: 'attire' },
      { id: 'do2',  time: '10:30 AM', title: 'Bridesmaids arrive',       where: 'Bridal suite',               icon: 'users',   kind: 'event' },
      { id: 'do3',  time: '12:00 PM', title: 'Light lunch & dressing',   where: 'Bridal suite',               icon: 'cake',    kind: 'tasting' },
      { id: 'do4',  time: '1:00 PM',  title: 'Photography · First look', where: 'Garden lawn',                icon: 'camera',  kind: 'venue' },
      { id: 'do5',  time: '2:30 PM',  title: 'Family portraits',         where: 'Cypress grove',              icon: 'camera',  kind: 'venue' },
      { id: 'do6',  time: '3:30 PM',  title: 'Guests arrive',            where: 'Welcome lawn',               icon: 'users',   kind: 'rsvp' },
      { id: 'do7',  time: '4:00 PM',  title: 'Ceremony',                 where: 'Cliffside terrace',          icon: 'rings',   kind: 'event', hero: true,
        note: 'Vows · ring exchange · processional · 32 minutes' },
      { id: 'do8',  time: '4:35 PM',  title: 'Cocktail hour',            where: 'Garden lawn',                icon: 'sparkle', kind: 'tasting' },
      { id: 'do9',  time: '5:45 PM',  title: 'Reception & dinner',       where: 'Hollow Tree barn',           icon: 'cake',    kind: 'event' },
      { id: 'do10', time: '7:30 PM',  title: 'Speeches & toasts',        where: 'Barn',                       icon: 'flag',    kind: 'event' },
      { id: 'do11', time: '8:15 PM',  title: 'First dance · cake',       where: 'Barn',                       icon: 'music',   kind: 'event' },
      { id: 'do12', time: '8:30 PM',  title: 'Dancing',                  where: 'Barn',                       icon: 'music',   kind: 'event' },
      { id: 'do13', time: '10:45 PM', title: 'Sparkler send-off',        where: 'Front drive',                icon: 'heart',   kind: 'attire' },
    ],
    emergencyContacts: [
      { id: 'ec1', role: 'Day-of planner', name: 'Nina Brock',  phone: '(415) 555-0142' },
      { id: 'ec2', role: 'Maid of honor',  name: 'Amelia Park', phone: '(310) 555-0188' },
      { id: 'ec3', role: 'Venue manager',  name: 'Lia Marsh',   phone: '(707) 555-0119' },
      { id: 'ec4', role: 'Officiant',      name: 'Father Joel', phone: '(707) 555-0193' },
    ],
    moodboard: {
      vibe:  MOODBOARD.vibe  || '',
      story: MOODBOARD.story || '',
      palette: (MOODBOARD.palette || []).map((c, i) => ({
        id: `c${i}-${Date.now().toString(36)}`,
        name: c.name, hex: c.hex,
      })),
      // Demo inspiration tiles are placeholders without real image data —
      // user adds real uploads via the Moodboard screen.
      inspiration: (MOODBOARD.inspiration || []).map((it, i) => ({
        id: `mi${i}-${Date.now().toString(36)}`,
        dataURL: '',         // empty = render placeholder gradient
        kind: it.kind || 'florals',
        label: it.label || '',
        span: it.span || 'short',
      })),
    },
    updatedAt: Date.now(),
  };
}

// Read once at module load so SSR-style early reads are stable.
function loadInitial() {
  try {
    const raw = localStorage.getItem(STORE_KEY);
    if (!raw) return makeDefaultState();
    const parsed = JSON.parse(raw);
    if (parsed.schemaVersion !== SCHEMA_VERSION) return makeDefaultState();
    // Merge over defaults so newly-added keys don't crash old saves.
    const defs = makeDefaultState();
    return {
      ...defs, ...parsed,
      wedding: { ...defs.wedding, ...parsed.wedding },
    };
  } catch {
    return makeDefaultState();
  }
}

function StoreProvider({ children }) {
  const [state, setState] = React.useState(loadInitial);

  // Debounced persist.
  React.useEffect(() => {
    const id = setTimeout(() => {
      try {
        const payload = { ...state, updatedAt: Date.now() };
        localStorage.setItem(STORE_KEY, JSON.stringify(payload));
      } catch {}
    }, 180);
    return () => clearTimeout(id);
  }, [state]);

  // Cross-artboard / cross-tab sync via the storage event.
  React.useEffect(() => {
    const onStorage = (e) => {
      if (e.key !== STORE_KEY || !e.newValue) return;
      try { setState(JSON.parse(e.newValue)); } catch {}
    };
    window.addEventListener('storage', onStorage);
    return () => window.removeEventListener('storage', onStorage);
  }, []);

  const value = React.useMemo(() => ({
    state,
    setWedding: (patch) =>
      setState(s => ({ ...s, wedding: { ...s.wedding, ...patch } })),
    toggleTask: (id) =>
      setState(s => ({
        ...s,
        tasks: s.tasks.map(t => {
          if (t.id !== id) return t;
          const next = t.status === 'done' ? 'assigned'
                     : t.status === 'inProgress' ? 'done'
                     : 'inProgress';
          return { ...t, status: next };
        }),
      })),
    setTaskStatus: (id, status) =>
      setState(s => ({
        ...s,
        tasks: s.tasks.map(t => t.id === id ? { ...t, status } : t),
      })),
    updateTask: (id, patch) =>
      setState(s => ({
        ...s,
        tasks: s.tasks.map(t => t.id === id ? { ...t, ...patch } : t),
      })),
    addTask: (task) =>
      setState(s => ({
        ...s,
        tasks: [
          {
            id: `t${Date.now().toString(36)}`,
            title: 'New to-do',
            kind: 'todo',
            due: 'No date',
            priority: 'med',
            assignedTo: 'shared',
            status: 'assigned',
            notes: '',
            dismissed: false,
            ...task,
          },
          ...s.tasks,
        ],
      })),
    dismissTask: (id) =>
      setState(s => ({
        ...s,
        tasks: s.tasks.map(t => t.id === id ? { ...t, dismissed: true } : t),
      })),

    // ─── Guests ─────────────────────────────────────────────
    addGuest: (g) =>
      setState(s => ({
        ...s,
        guests: [
          {
            id: `g${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`,
            name: '', group: '', status: 'pending', meal: '',
            dietary: '', plus: false, plusName: '', partySize: 1, table: null,
            ...g,
          },
          ...s.guests,
        ],
      })),
    updateGuest: (id, patch) =>
      setState(s => ({
        ...s,
        guests: s.guests.map(g => g.id === id ? { ...g, ...patch } : g),
      })),
    deleteGuest: (id) =>
      setState(s => ({
        ...s,
        guests: s.guests.filter(g => g.id !== id),
      })),

    // ─── Budget categories ──────────────────────────────────
    addCategory: (c) =>
      setState(s => ({
        ...s,
        budgetCategories: [
          ...s.budgetCategories,
          {
            id: `bc${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`,
            name: 'New category', planned: 0, spent: 0,
            icon: 'sparkle', note: '',
            ...c,
          },
        ],
      })),
    updateCategory: (id, patch) =>
      setState(s => ({
        ...s,
        budgetCategories: s.budgetCategories.map(c => c.id === id ? { ...c, ...patch } : c),
      })),
    deleteCategory: (id) =>
      setState(s => ({
        ...s,
        budgetCategories: s.budgetCategories.filter(c => c.id !== id),
      })),

    // ─── Vendors ────────────────────────────────────────────
    addVendor: (v) =>
      setState(s => ({
        ...s,
        vendors: [
          ...s.vendors,
          {
            id: `v${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`,
            name: 'New vendor', role: '', contact: '',
            status: 'researching', paid: 0, total: 0, next: '', notes: '',
            ...v,
          },
        ],
      })),
    updateVendor: (id, patch) =>
      setState(s => ({
        ...s,
        vendors: s.vendors.map(v => v.id === id ? { ...v, ...patch } : v),
      })),
    deleteVendor: (id) =>
      setState(s => ({
        ...s,
        vendors: s.vendors.filter(v => v.id !== id),
      })),

    // ─── Tables ─────────────────────────────────────────────
    addTable: (t) =>
      setState(s => {
        const nextNum = s.tables.reduce((m, x) => Math.max(m, x.number || 0), 0) + 1;
        const capacity = t.capacity || 8;
        return {
          ...s,
          tables: [
            ...s.tables,
            {
              id: `t${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`,
              number: nextNum,
              name: `Table ${nextNum}`,
              kind: 'friends',
              capacity,
              seats: new Array(capacity).fill(null),
              ...t,
            },
          ],
        };
      }),
    updateTable: (id, patch) =>
      setState(s => ({
        ...s,
        tables: s.tables.map(t => {
          if (t.id !== id) return t;
          const next = { ...t, ...patch };
          // If capacity changed, resize the seats array.
          if (patch.capacity != null && patch.capacity !== t.capacity) {
            const seats = (t.seats || []).slice(0, patch.capacity);
            while (seats.length < patch.capacity) seats.push(null);
            next.seats = seats;
          }
          return next;
        }),
      })),
    deleteTable: (id) =>
      setState(s => ({
        ...s,
        tables: s.tables.filter(t => t.id !== id),
      })),
    // Seat a guest at the first empty seat of a table.
    seatGuest: (tableId, guestName) =>
      setState(s => ({
        ...s,
        tables: s.tables.map(t => {
          if (t.id !== tableId) return t;
          const seats = [...(t.seats || [])];
          const idx = seats.findIndex(x => !x);
          if (idx === -1) return t; // full — caller should check
          seats[idx] = guestName;
          return { ...t, seats };
        }),
        guests: s.guests.map(g =>
          g.name === guestName ? { ...g, table: tableId } : g),
      })),
    // Remove a guest from whichever table they're sitting at.
    unseatGuest: (guestName) =>
      setState(s => ({
        ...s,
        tables: s.tables.map(t => ({
          ...t,
          seats: (t.seats || []).map(x => x === guestName ? null : x),
        })),
        guests: s.guests.map(g =>
          g.name === guestName ? { ...g, table: null } : g),
      })),

    // ─── Events ─────────────────────────────────────────────
    addEvent: (e) =>
      setState(s => ({
        ...s,
        events: [
          ...(s.events || []),
          {
            id: `e${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`,
            dateISO: new Date().toISOString(),
            title: '', time: '', kind: 'event', note: '',
            ...e,
          },
        ],
      })),
    updateEvent: (id, patch) =>
      setState(s => ({
        ...s,
        events: (s.events || []).map(e => e.id === id ? { ...e, ...patch } : e),
      })),
    deleteEvent: (id) =>
      setState(s => ({
        ...s,
        events: (s.events || []).filter(e => e.id !== id),
      })),

    // ─── Party ──────────────────────────────────────────────
    addPartyMember: (p) =>
      setState(s => ({
        ...s,
        party: [
          ...(s.party || []),
          {
            id: `p${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`,
            name: '', role: '', section: 'bridal', side: 'sloane',
            ...p,
          },
        ],
      })),
    updatePartyMember: (id, patch) =>
      setState(s => ({
        ...s,
        party: (s.party || []).map(p => p.id === id ? { ...p, ...patch } : p),
      })),
    deletePartyMember: (id) =>
      setState(s => ({
        ...s,
        party: (s.party || []).filter(p => p.id !== id),
      })),

    // ─── Day-of timeline ────────────────────────────────────
    addTimelineEvent: (e) =>
      setState(s => ({
        ...s,
        dayOfTimeline: [
          ...(s.dayOfTimeline || []),
          {
            id: `do${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`,
            time: '', title: '', where: '', kind: 'event', icon: 'sparkle', note: '',
            ...e,
          },
        ],
      })),
    updateTimelineEvent: (id, patch) =>
      setState(s => ({
        ...s,
        dayOfTimeline: (s.dayOfTimeline || []).map(e => e.id === id ? { ...e, ...patch } : e),
      })),
    deleteTimelineEvent: (id) =>
      setState(s => ({
        ...s,
        dayOfTimeline: (s.dayOfTimeline || []).filter(e => e.id !== id),
      })),

    // ─── Emergency contacts ─────────────────────────────────
    addEmergencyContact: (c) =>
      setState(s => ({
        ...s,
        emergencyContacts: [
          ...(s.emergencyContacts || []),
          {
            id: `ec${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`,
            role: '', name: '', phone: '',
            ...c,
          },
        ],
      })),
    updateEmergencyContact: (id, patch) =>
      setState(s => ({
        ...s,
        emergencyContacts: (s.emergencyContacts || []).map(c => c.id === id ? { ...c, ...patch } : c),
      })),
    deleteEmergencyContact: (id) =>
      setState(s => ({
        ...s,
        emergencyContacts: (s.emergencyContacts || []).filter(c => c.id !== id),
      })),

    // ─── Moodboard ──────────────────────────────────────────
    setMoodboard: (patch) =>
      setState(s => ({
        ...s,
        moodboard: { ...(s.moodboard || {}), ...patch },
      })),
    addColor: (c) =>
      setState(s => ({
        ...s,
        moodboard: {
          ...(s.moodboard || {}),
          palette: [
            ...((s.moodboard && s.moodboard.palette) || []),
            { id: `c${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`,
              name: c.name || 'New color',
              hex:  c.hex  || '#cccccc',
            },
          ],
        },
      })),
    updateColor: (id, patch) =>
      setState(s => ({
        ...s,
        moodboard: {
          ...(s.moodboard || {}),
          palette: ((s.moodboard && s.moodboard.palette) || [])
            .map(c => c.id === id ? { ...c, ...patch } : c),
        },
      })),
    deleteColor: (id) =>
      setState(s => ({
        ...s,
        moodboard: {
          ...(s.moodboard || {}),
          palette: ((s.moodboard && s.moodboard.palette) || [])
            .filter(c => c.id !== id),
        },
      })),
    addInspiration: (it) =>
      setState(s => ({
        ...s,
        moodboard: {
          ...(s.moodboard || {}),
          inspiration: [
            ...((s.moodboard && s.moodboard.inspiration) || []),
            { id: `mi${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`,
              dataURL: '', kind: 'florals', label: '', span: 'short', ...it },
          ],
        },
      })),
    updateInspiration: (id, patch) =>
      setState(s => ({
        ...s,
        moodboard: {
          ...(s.moodboard || {}),
          inspiration: ((s.moodboard && s.moodboard.inspiration) || [])
            .map(it => it.id === id ? { ...it, ...patch } : it),
        },
      })),
    deleteInspiration: (id) =>
      setState(s => ({
        ...s,
        moodboard: {
          ...(s.moodboard || {}),
          inspiration: ((s.moodboard && s.moodboard.inspiration) || [])
            .filter(it => it.id !== id),
        },
      })),

    resetDemo: () => setState(makeDefaultState()),
    startFresh: () => setState(makeEmptyState()),
  }), [state]);

  return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>;
}

// ─── Derived hooks ──────────────────────────────────────────

function useStore() {
  const ctx = React.useContext(StoreContext);
  if (!ctx) {
    // Safety fallback for any consumer outside the provider — return defaults
    // without persistence. Keeps screens rendering even if the provider is missing.
    return {
      state: makeDefaultState(),
      setWedding: () => {}, toggleTask: () => {},
      dismissTask: () => {}, resetDemo: () => {},
    };
  }
  return ctx;
}

function useWedding() {
  const { state, setWedding } = useStore();
  const w = state.wedding;
  const date = w.dateISO ? new Date(w.dateISO) : null;
  const now = new Date();
  const dayMs = 86400000;
  const daysUntil = date
    ? Math.max(0, Math.ceil((date - now) / dayMs))
    : null;
  return {
    ...w,
    date,
    daysUntil,
    isEmpty: !w.brides && !w.groom && !w.dateISO,
    dateLabel: date
      ? date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })
      : '',
    dayName:   date ? date.toLocaleDateString('en-US', { weekday: 'long' }) : '',
    couple:    [w.brides, w.groom].filter(Boolean).join(' & '),
    setWedding,
  };
}

function useTasks() {
  const { state, toggleTask, setTaskStatus, dismissTask, addTask, updateTask } = useStore();
  const tasks = state.tasks || [];
  return {
    all:    tasks,
    active: tasks.filter(t => !t.dismissed && t.status !== 'done'),
    done:   tasks.filter(t => !t.dismissed && t.status === 'done'),
    inProgress: tasks.filter(t => !t.dismissed && t.status === 'inProgress'),
    assigned:   tasks.filter(t => !t.dismissed && t.status === 'assigned'),
    toggleTask, setTaskStatus, dismissTask, addTask, updateTask,
  };
}

// Guests, plus derived RSVP stats.
function useGuests() {
  const { state, addGuest, updateGuest, deleteGuest } = useStore();
  const all = state.guests;

  // Sum effective party-size per status.
  const sum = (pred) =>
    all.filter(pred).reduce((n, g) => n + (g.partySize || 1), 0);
  const attending = sum(g => g.status === 'attending');
  const declined  = sum(g => g.status === 'declined');
  const pending   = sum(g => g.status === 'pending');
  const total     = attending + declined + pending;

  // Meal & dietary roll-ups.
  const meals = {};
  all.forEach(g => {
    if (g.status === 'attending' && g.meal) {
      meals[g.meal] = (meals[g.meal] || 0) + (g.partySize || 1);
    }
  });
  const dietaryCounts = {};
  all.forEach(g => {
    if (!g.dietary) return;
    g.dietary.split(/[,;]/).map(s => s.trim()).filter(Boolean).forEach(d => {
      dietaryCounts[d] = (dietaryCounts[d] || 0) + (g.partySize || 1);
    });
  });
  const dietary = Object.entries(dietaryCounts)
    .map(([label, count]) => ({ label, count }))
    .sort((a, b) => b.count - a.count);

  return {
    all,
    households: all.length,
    attending, declined, pending, total,
    meals, dietary,
    alerts: dietary.length,
    addGuest, updateGuest, deleteGuest,
  };
}

// Budget categories with derived stats.
function useBudget() {
  const { state, addCategory, updateCategory, deleteCategory } = useStore();
  const cats = state.budgetCategories || [];
  const categories = cats.map(c => {
    let status;
    if (!c.planned || c.spent === 0) status = c.planned ? 'open' : 'open';
    else if (c.spent > c.planned)    status = 'over';
    else if (c.spent >= c.planned)   status = 'paid';
    else                              status = 'partial';
    return { ...c, status, pct: c.planned ? c.spent / c.planned : 0 };
  });

  const total       = state.wedding.budget || 0;
  const planned     = categories.reduce((n, c) => n + (c.planned || 0), 0);
  const spent       = categories.reduce((n, c) => n + (c.spent   || 0), 0);
  const remaining   = total - spent;
  // Proxy for paidToDate: only categories marked 'paid' have been settled.
  const paidToDate  = categories
    .filter(c => c.status === 'paid')
    .reduce((n, c) => n + c.spent, 0);
  const overspend   = categories
    .filter(c => c.status === 'over')
    .reduce((n, c) => n + (c.spent - c.planned), 0);

  const statusCounts = { paid: 0, partial: 0, over: 0, open: 0 };
  categories.forEach(c => { statusCounts[c.status]++; });

  return {
    categories,
    total, planned, spent, remaining, paidToDate, overspend,
    statusCounts,
    addCategory, updateCategory, deleteCategory,
  };
}

// Vendors with derived stats.
function useVendors() {
  const { state, addVendor, updateVendor, deleteVendor } = useStore();
  const all = state.vendors || [];

  const counts = { researching: 0, booked: 0, partial: 0, due: 0, paid: 0 };
  all.forEach(v => { counts[v.status] = (counts[v.status] || 0) + 1; });

  const paidToDate = all.reduce((n, v) => n + (v.paid || 0), 0);
  const totalCommitted = all.reduce((n, v) => n + (v.total || 0), 0);
  const owed = Math.max(0, totalCommitted - paidToDate);
  const bookedCount = counts.booked + counts.partial + counts.due + counts.paid;

  return {
    all,
    counts,
    bookedCount,
    paidToDate,
    totalCommitted,
    owed,
    addVendor, updateVendor, deleteVendor,
  };
}

// Tables with derived seat counts.
function useTables() {
  const { state, addTable, updateTable, deleteTable, seatGuest, unseatGuest } = useStore();
  const all = state.tables || [];
  const totalSeats   = all.reduce((n, t) => n + (t.capacity || 0), 0);
  const filledSeats  = all.reduce(
    (n, t) => n + (t.seats || []).filter(Boolean).length, 0
  );
  // Attending guests who aren't yet sitting in any table's seats array.
  const seatedNames = new Set();
  all.forEach(t => (t.seats || []).forEach(name => { if (name) seatedNames.add(name); }));
  const unassignedGuests = (state.guests || [])
    .filter(g => g.status === 'attending' && !seatedNames.has(g.name));
  const attendingHeads = (state.guests || [])
    .filter(g => g.status === 'attending')
    .reduce((n, g) => n + (g.partySize || 1), 0);
  const unassigned = Math.max(0, attendingHeads - filledSeats);
  return {
    all,
    totalSeats, filledSeats, unassigned, attendingHeads,
    unassignedGuests,
    addTable, updateTable, deleteTable,
    seatGuest, unseatGuest,
  };
}

// Calendar events keyed by date.
function useEvents() {
  const { state, addEvent, updateEvent, deleteEvent } = useStore();
  const all = state.events || [];
  // Stable date key helper.
  const keyOf = (d) =>
    d ? new Date(d).toISOString().slice(0, 10) : '';
  const byDay = {};
  all.forEach(e => {
    const k = keyOf(e.dateISO);
    (byDay[k] = byDay[k] || []).push(e);
  });
  // Stable sort by time within a day.
  Object.values(byDay).forEach(arr => arr.sort((a, b) => (a.time > b.time ? 1 : -1)));

  const now = new Date();
  const todayKey = keyOf(now);
  const upcoming = all
    .filter(e => keyOf(e.dateISO) >= todayKey)
    .sort((a, b) => a.dateISO < b.dateISO ? -1 : 1);

  return {
    all, byDay, upcoming,
    keyOf,
    addEvent, updateEvent, deleteEvent,
  };
}

// Wedding party grouped by section.
function useParty() {
  const { state, addPartyMember, updatePartyMember, deletePartyMember } = useStore();
  const all = state.party || [];
  const bridal  = all.filter(p => p.section === 'bridal');
  const groom   = all.filter(p => p.section === 'groom');
  const family  = all.filter(p => p.section === 'family');
  const special = all.filter(p => p.section === 'special');
  return {
    all, bridal, groom, family, special,
    total: all.length,
    addPartyMember, updatePartyMember, deletePartyMember,
  };
}

// Day-of timeline, sorted by time (HH:MM AM/PM strings are sortable when normalized).
function useDayOfTimeline() {
  const { state, addTimelineEvent, updateTimelineEvent, deleteTimelineEvent } = useStore();
  const all = state.dayOfTimeline || [];
  // Sort by parsed time so a "7:00 AM" comes before "10:30 AM".
  const parseT = (t) => {
    const m = /(\d{1,2}):(\d{2})\s*(AM|PM)?/i.exec(t || '');
    if (!m) return 9999;
    let h = parseInt(m[1], 10);
    const mm = parseInt(m[2], 10);
    const ap = (m[3] || '').toUpperCase();
    if (ap === 'PM' && h !== 12) h += 12;
    if (ap === 'AM' && h === 12) h = 0;
    return h * 60 + mm;
  };
  const sorted = [...all].sort((a, b) => parseT(a.time) - parseT(b.time));
  return {
    all: sorted,
    addTimelineEvent, updateTimelineEvent, deleteTimelineEvent,
  };
}

// Emergency contacts list.
function useEmergencyContacts() {
  const { state, addEmergencyContact, updateEmergencyContact, deleteEmergencyContact } = useStore();
  return {
    all: state.emergencyContacts || [],
    addEmergencyContact, updateEmergencyContact, deleteEmergencyContact,
  };
}

// Moodboard with palette + vibe story.
function useMoodboard() {
  const { state, setMoodboard, addColor, updateColor, deleteColor,
          addInspiration, updateInspiration, deleteInspiration } = useStore();
  const m = state.moodboard || { vibe: '', story: '', palette: [], inspiration: [] };
  return {
    vibe:        m.vibe || '',
    story:       m.story || '',
    palette:     m.palette || [],
    inspiration: m.inspiration || [],
    setMoodboard, addColor, updateColor, deleteColor,
    addInspiration, updateInspiration, deleteInspiration,
  };
}

Object.assign(window, {
  StoreProvider, StoreContext,
  useStore, useWedding, useTasks, useGuests, useBudget, useVendors,
  useTables, useEvents, useParty, useMoodboard,
  useDayOfTimeline, useEmergencyContacts,
  makeDefaultState, makeEmptyState,
});
