// data.jsx — seed events, categories, sessions

// "Today" anchor — real current date, normalized to midnight local time.
const CHRONO_TODAY = (() => { const d = new Date(); d.setHours(0,0,0,0); return d; })();

// Categories are user-owned and persist in localStorage. A fresh account
// starts with NONE — the user (or the AI, with the user's explicit yes)
// creates them as real events come in. Events that don't match any category
// stay uncategorized and render grey (see ChronoTokens.events.grey).
const CATEGORY_STORE_KEY = 'chrono.categories.v1';
function loadCategories() {
  try {
    const raw = localStorage.getItem(CATEGORY_STORE_KEY);
    if (raw) {
      const arr = JSON.parse(raw);
      if (Array.isArray(arr)) {
        // Keep only well-formed rows; normalize the count field.
        return arr
          .filter(c => c && typeof c === 'object' && c.id && c.name)
          .map(c => ({ id: String(c.id), name: String(c.name), color: String(c.color || 'blue'), count: 0 }));
      }
    }
  } catch (e) { /* fall through to empty */ }
  return [];
}
// Mutated in place (push/splice) so every existing `CATEGORIES.map/find/some`
// call site keeps working against a live array.
const CATEGORIES = loadCategories();

const CATEGORY_LISTENERS = new Set();
function persistCategories() {
  try { localStorage.setItem(CATEGORY_STORE_KEY, JSON.stringify(CATEGORIES)); } catch (e) {}
}
function notifyCategoriesChanged() {
  persistCategories();
  CATEGORY_LISTENERS.forEach(cb => { try { cb(); } catch (e) {} });
}
// Build a stable, unique id from a display name.
function slugifyCategory(name) {
  const base = String(name || '').toLowerCase().trim()
    .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'category';
  let id = base, n = 2;
  while (CATEGORIES.some(c => c.id === id)) id = `${base}-${n++}`;
  return id;
}
// Add a category (idempotent on id/name). Returns the category row — the
// existing one if a match is already present, so the AI flow never dupes.
function addCategory({ id, name, color } = {}) {
  const finalName = String(name || 'Category').slice(0, 40).trim() || 'Category';
  const validColor = typeof ChronoTokens !== 'undefined' && ChronoTokens.events && ChronoTokens.events[color];
  const finalColor = validColor ? color : 'blue';
  const existing = CATEGORIES.find(c =>
    (id && c.id === id) || c.name.toLowerCase() === finalName.toLowerCase());
  if (existing) return existing;
  const cat = {
    id: (id && !CATEGORIES.some(c => c.id === id)) ? String(id) : slugifyCategory(finalName),
    name: finalName, color: finalColor, count: 0,
  };
  CATEGORIES.push(cat);
  notifyCategoriesChanged();
  return cat;
}
function removeCategory(id) {
  const i = CATEGORIES.findIndex(c => c.id === id);
  if (i >= 0) { CATEGORIES.splice(i, 1); notifyCategoriesChanged(); return true; }
  return false;
}
// Re-render hook for any view that lists categories (sidebar, composer,
// categories page) so adds/removes show up immediately.
function useCategoryList() {
  const [, setTick] = React.useState(0);
  React.useEffect(() => {
    const cb = () => setTick(t => t + 1);
    CATEGORY_LISTENERS.add(cb);
    return () => CATEGORY_LISTENERS.delete(cb);
  }, []);
  return CATEGORIES;
}

// Day offsets from CHRONO_TODAY → events for that day
// Format: { title, start: 'HH:MM', end: 'HH:MM', color, category, priority, location, note }
function makeEvents() {
  // Blank slate — a fresh install has no events.
  return {};
  /* eslint-disable no-unreachable */
  const out = {};
  const set = (offset, evs) => { out[offset] = evs; };

  set(-2, [
    { title: 'Algorithms — Lecture 18', start: '09:00', end: '10:30', color: 'purple', category: 'study', priority: 'med' },
    { title: 'Gym',                     start: '07:00', end: '08:00', color: 'green', category: 'health' },
    { title: 'Standup',                 start: '11:00', end: '11:15', color: 'red', category: 'meet' },
  ]);
  set(-1, [
    { title: 'Design review',           start: '10:00', end: '11:00', color: 'red', category: 'meet', priority: 'high' },
    { title: 'CS 374 problem set',      start: '14:00', end: '16:30', color: 'yellow', category: 'canvas' },
    { title: 'Dinner w/ Leila',         start: '19:00', end: '20:30', color: 'pink', category: 'personal' },
  ]);
  set(0, [
    { title: 'Morning run',             start: '06:30', end: '07:15', color: 'green',  category: 'health' },
    { title: 'Deep work — Project X',   start: '09:00', end: '11:30', color: 'orange', category: 'creative', priority: 'high', note: 'Finish API spec draft' },
    { title: 'Standup',                 start: '11:30', end: '11:45', color: 'red',    category: 'meet' },
    { title: 'Lunch (block)',           start: '12:30', end: '13:30', color: 'blue',   category: 'work' },
    { title: 'AI seminar',              start: '14:00', end: '15:30', color: 'purple', category: 'study', priority: 'med' },
    { title: '1:1 with Naomi',          start: '15:45', end: '16:15', color: 'red',    category: 'meet', location: 'Zoom' },
    { title: 'Review PR #482',          start: '16:30', end: '17:30', color: 'blue',   category: 'work' },
    { title: 'Yoga',                    start: '19:00', end: '20:00', color: 'green',  category: 'health' },
  ]);
  set(1, [
    { title: 'Coffee w/ Marco',         start: '08:00', end: '09:00', color: 'pink',   category: 'personal' },
    { title: 'Linear Algebra final',    start: '10:00', end: '12:00', color: 'yellow', category: 'canvas', priority: 'high' },
    { title: 'Roadmap planning',        start: '14:00', end: '15:30', color: 'red',    category: 'meet' },
    { title: 'Studio time',             start: '20:00', end: '22:00', color: 'orange', category: 'creative' },
  ]);
  set(2, [
    { title: 'PT appointment',          start: '09:00', end: '10:00', color: 'green',  category: 'health' },
    { title: 'Quarterly review',        start: '11:00', end: '12:30', color: 'red',    category: 'meet', priority: 'high' },
    { title: 'Reading group',           start: '17:00', end: '18:00', color: 'purple', category: 'study' },
  ]);
  set(3, [
    { title: 'Long run',                start: '07:00', end: '08:30', color: 'green',  category: 'health' },
    { title: 'Family brunch',           start: '11:00', end: '13:00', color: 'pink',   category: 'personal' },
  ]);
  set(4, [
    { title: 'Hiking — Mt Tam',         start: '08:00', end: '13:00', color: 'pink',   category: 'personal' },
  ]);
  set(5, [
    { title: 'Sprint planning',         start: '10:00', end: '11:30', color: 'red',    category: 'meet' },
    { title: 'OSes Lab 4',              start: '14:00', end: '17:00', color: 'yellow', category: 'canvas' },
  ]);
  set(6, [
    { title: 'Deep work',               start: '09:00', end: '12:00', color: 'orange', category: 'creative', priority: 'high' },
    { title: 'Therapy',                 start: '14:00', end: '15:00', color: 'green',  category: 'health' },
  ]);
  set(7, [
    { title: 'Conference talk',         start: '10:00', end: '11:00', color: 'blue',   category: 'work', priority: 'high' },
  ]);
  set(8, [
    { title: 'Book club',               start: '19:00', end: '20:30', color: 'purple', category: 'study' },
  ]);
  set(10, [
    { title: 'Off-site',                start: '09:00', end: '17:00', color: 'blue',   category: 'work' },
  ]);
  // Some random sprinkles backwards/forwards
  set(-5, [{ title: 'Dentist', start:'09:00', end:'10:00', color:'green', category:'health'}]);
  set(-3, [{ title: 'PR review', start:'15:00', end:'16:00', color:'blue', category:'work'}]);
  set(-7, [{ title: 'Birthday', start:'19:00', end:'22:00', color:'pink', category:'personal'}]);
  set(11, [{ title: 'Demo day', start:'14:00', end:'16:00', color:'red', category:'meet', priority:'high'}]);
  set(13, [{ title: 'Workshop', start:'10:00', end:'12:00', color:'purple', category:'study'}]);
  set(14, [{ title: 'Concert', start:'20:00', end:'23:00', color:'pink', category:'personal'}]);

  return out;
}

const EVENTS_BY_OFFSET = makeEvents();

function dateAtOffset(offset) {
  const d = new Date(CHRONO_TODAY);
  d.setDate(d.getDate() + offset);
  return d;
}
function offsetForDate(d) {
  const ms = d - CHRONO_TODAY;
  return Math.round(ms / (1000*60*60*24));
}
function eventsForOffset(off) { return EVENTS_BY_OFFSET[off] || []; }

// ── US federal holidays ────────────────────────────────────────────────
// Computed from rules so any year works. Cached per year.
const _HOLIDAY_CACHE = new Map();
function usHolidaysForYear(year) {
  if (_HOLIDAY_CACHE.has(year)) return _HOLIDAY_CACHE.get(year);
  const out = new Map();
  const key = (mo, day) => `${year}-${String(mo).padStart(2,'0')}-${String(day).padStart(2,'0')}`;

  // Fixed-date holidays
  out.set(key(1, 1),  "New Year's Day");
  out.set(key(6, 19), 'Juneteenth');
  out.set(key(7, 4),  'Independence Day');
  out.set(key(11, 11),'Veterans Day');
  out.set(key(12, 25),'Christmas Day');

  // Nth weekday of month (weekday: 0=Sun..6=Sat)
  const nthWeekday = (mo, wd, n) => {
    const first = new Date(year, mo-1, 1);
    const shift = (wd - first.getDay() + 7) % 7;
    return 1 + shift + (n-1)*7;
  };
  out.set(key(1,  nthWeekday(1,  1, 3)), 'Martin Luther King Jr. Day'); // 3rd Mon Jan
  out.set(key(2,  nthWeekday(2,  1, 3)), "Presidents' Day");             // 3rd Mon Feb
  out.set(key(9,  nthWeekday(9,  1, 1)), 'Labor Day');                   // 1st Mon Sep
  out.set(key(10, nthWeekday(10, 1, 2)), 'Columbus Day');                // 2nd Mon Oct
  out.set(key(11, nthWeekday(11, 4, 4)), 'Thanksgiving Day');            // 4th Thu Nov

  // Memorial Day — last Monday of May.
  const lastMay = new Date(year, 5, 0); // day 0 of June = last day of May
  const memOffset = (lastMay.getDay() - 1 + 7) % 7;
  out.set(key(5, lastMay.getDate() - memOffset), 'Memorial Day');

  _HOLIDAY_CACHE.set(year, out);
  return out;
}
function holidayForDate(d) {
  const k = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
  return usHolidaysForYear(d.getFullYear()).get(k) || null;
}
function holidayForOffset(off) { return holidayForDate(dateAtOffset(off)); }

// ── Persisted reactive stores (profile / settings / integrations) ────
function makePersistedStore(key, defaults) {
  let state;
  try {
    const raw = localStorage.getItem(key);
    state = raw ? { ...defaults, ...JSON.parse(raw) } : { ...defaults };
  } catch (e) { state = { ...defaults }; }
  const listeners = new Set();
  const notify = () => listeners.forEach(cb => { try { cb(); } catch (e) {} });
  return {
    get: () => state,
    set: (patch) => {
      state = { ...state, ...patch };
      try { localStorage.setItem(key, JSON.stringify(state)); } catch (e) {}
      notify();
    },
    reset: () => {
      state = { ...defaults };
      try { localStorage.removeItem(key); } catch (e) {}
      notify();
    },
    use: () => {
      const [, setTick] = React.useState(0);
      React.useEffect(() => {
        const cb = () => setTick(t => t + 1);
        listeners.add(cb);
        return () => listeners.delete(cb);
      }, []);
      return state;
    },
    // Subscribe outside React (used by the account-sync engine). Returns an
    // unsubscribe fn.
    subscribe: (cb) => { listeners.add(cb); return () => listeners.delete(cb); },
  };
}

const profileStore = makePersistedStore('chrono.profile.v1', {
  name: 'Your name',
  email: 'you@chrono.app',
  plan: 'Pro', // Beta: every tester is Pro. Tiers are hidden until after beta.
});
const settingsStore = makePersistedStore('chrono.settings.v1', {
  weekStartsOn: 0,        // 0=Sun, 1=Mon
  defaultDuration: 60,    // minutes
  showHolidays: true,
  timeFormat: '12',       // '12' | '24'
  sounds: true,
  notifLead: 10,          // minutes
  aiBufferMin: 10,
  aiShowReasoning: true,
  aiFollowups: 'auto',    // 'auto' = act on vague prompts with smart defaults; 'ask' = ask clarifying questions first
  aiPersonality: 'concierge', // concierge | copilot | silent
  focusSessionMin: 25,    // default focus timer length
  workStart: '08:00',     // derived from sleep schedule
  workEnd:   '22:00',
});
const onboardingStore = makePersistedStore('chrono.onboarding.v1', {
  done: false,
  tutorialDone: false,
  betaNoticeSeen: false, // one-time "you're in the beta" notice after the tutorial
  answers: {},
});
const integrationsStore = makePersistedStore('chrono.integrations.v1', {
  google: false,
  icloud: false,
  canvas: false,
  outlook: false,
  canvasSchool: '',
  canvasCourses: [],      // [{ id, code, name, color, assignments: [{title, dueOff, due, weight}] }]
});

function useProfile()           { return profileStore.use(); }
function setProfile(patch)      { profileStore.set(patch); }
function useSettings()          { return settingsStore.use(); }
function setSettings(patch)     { settingsStore.set(patch); }
function resetSettings()        { settingsStore.reset(); }
function useIntegrations()      { return integrationsStore.use(); }
function setIntegrations(patch) { integrationsStore.set(patch); }
function useOnboarding()        { return onboardingStore.use(); }
function setOnboarding(patch)   { onboardingStore.set(patch); }
function resetOnboarding()      { onboardingStore.reset(); }

// Tutorial pub-sub: lets the GuidedTutorial drive the desktop's active view
// without lifting state out of ChronoDesktop.
const TUTORIAL_CHANNEL = { listeners: new Set() };
function setTutorialView(v) { TUTORIAL_CHANNEL.listeners.forEach(cb => { try { cb(v); } catch (e) {} }); }
function useTutorialView(handler) {
  React.useEffect(() => {
    TUTORIAL_CHANNEL.listeners.add(handler);
    return () => TUTORIAL_CHANNEL.listeners.delete(handler);
  }, [handler]);
}

// Tutorial → AI panel: lets the guided tour force the assistant popout open
// (and closed again on exit) without lifting aiOpen state out of ChronoDesktop.
const TUTORIAL_AI_CHANNEL = { listeners: new Set() };
function setTutorialAI(open) { TUTORIAL_AI_CHANNEL.listeners.forEach(cb => { try { cb(!!open); } catch (e) {} }); }
function useTutorialAI(handler) {
  React.useEffect(() => {
    TUTORIAL_AI_CHANNEL.listeners.add(handler);
    return () => TUTORIAL_AI_CHANNEL.listeners.delete(handler);
  }, [handler]);
}

// Per-category live stats. Walks the live store on every call (cheap at our
// scale — single-user, hundreds of events max) so counts/hours always match
// what's on the calendar. Falls back to color-based bucketing for events
// that have no explicit category — keeps the AI's color-only outputs from
// showing up as "uncategorized".
function categoriesWithCounts() {
  const stats = new Map(CATEGORIES.map(c => [c.id, { count: 0, minutes: 0 }]));
  const colorToId = new Map(CATEGORIES.map(c => [c.color, c.id]));
  for (const k of Object.keys(EVENTS_BY_OFFSET)) {
    for (const e of EVENTS_BY_OFFSET[k]) {
      const id = e.category && stats.has(e.category)
        ? e.category
        : colorToId.get(e.color);
      if (!id) continue;
      const s = stats.get(id);
      s.count += 1;
      s.minutes += Math.max(0, tm(e.end) - tm(e.start));
    }
  }
  return CATEGORIES.map(c => {
    const s = stats.get(c.id);
    return { ...c, count: s.count, hours: Math.round(s.minutes / 60 * 10) / 10 };
  });
}
function useCategories() {
  useEvents();       // re-render on event mutations
  useCategoryList(); // re-render on category add/remove
  return categoriesWithCounts();
}

// Aggregate stats derived from the live event store.
function eventStats() {
  let totalEvents = 0, totalMinutes = 0;
  for (const k of Object.keys(EVENTS_BY_OFFSET)) {
    const arr = EVENTS_BY_OFFSET[k];
    totalEvents += arr.length;
    arr.forEach(e => { totalMinutes += tm(e.end) - tm(e.start); });
  }
  return { totalEvents, totalHours: Math.round(totalMinutes / 60 * 10) / 10 };
}

// Mutable event store — manual add/edit notifies subscribers so calendar views re-render.
const EVENT_LISTENERS = new Set();
function notifyEventsChanged() { EVENT_LISTENERS.forEach(cb => { try { cb(); } catch (e) {} }); }
function addEventAtOffset(off, ev) {
  if (!EVENTS_BY_OFFSET[off]) EVENTS_BY_OFFSET[off] = [];
  EVENTS_BY_OFFSET[off].push(ev);
  notifyEventsChanged();
  return ev; // return the stored ref so callers can re-tag it later (AI category flow)
}
// Re-tag an existing event in place (used when the user accepts an AI-proposed
// category for events that were just added uncategorized).
function setEventCategory(ref, category, color) {
  if (!ref || typeof ref !== 'object') return false;
  ref.category = category;
  if (color) ref.color = color;
  notifyEventsChanged();
  return true;
}
function updateEventByRef(originalEv, newOff, newEv) {
  for (const k of Object.keys(EVENTS_BY_OFFSET)) {
    const arr = EVENTS_BY_OFFSET[k];
    const i = arr.indexOf(originalEv);
    if (i >= 0) { arr.splice(i, 1); break; }
  }
  if (!EVENTS_BY_OFFSET[newOff]) EVENTS_BY_OFFSET[newOff] = [];
  EVENTS_BY_OFFSET[newOff].push(newEv);
  notifyEventsChanged();
}
function deleteEventByRef(ev) {
  for (const k of Object.keys(EVENTS_BY_OFFSET)) {
    const arr = EVENTS_BY_OFFSET[k];
    const i = arr.indexOf(ev);
    if (i >= 0) { arr.splice(i, 1); notifyEventsChanged(); return true; }
  }
  return false;
}

// ── Canvas LMS → calendar ────────────────────────────────────────────────
// Materialize Canvas assignment due dates as real calendar events so they
// show up in the Week/Month views — not just the Canvas tab. Events carry
// source:'canvas' so we can wipe & re-sync idempotently (connect, reconnect,
// disconnect) without touching the user's own events.
const CANVAS_DUE_TIME = { start: '17:00', end: '18:00' }; // shown as an afternoon deadline block
function setCanvasEvents(courses) {
  // 1. Remove any previously-synced Canvas events.
  for (const k of Object.keys(EVENTS_BY_OFFSET)) {
    const kept = EVENTS_BY_OFFSET[k].filter(e => e.source !== 'canvas');
    if (kept.length) EVENTS_BY_OFFSET[k] = kept;
    else delete EVENTS_BY_OFFSET[k];
  }
  // 2. Add one event per assignment at its due-date offset.
  (courses || []).forEach(c => {
    (c.assignments || []).forEach(a => {
      if (a.dueOff == null) return;
      const off = a.dueOff;
      if (!EVENTS_BY_OFFSET[off]) EVENTS_BY_OFFSET[off] = [];
      EVENTS_BY_OFFSET[off].push({
        title: `${a.title} · ${c.code}`,
        start: CANVAS_DUE_TIME.start, end: CANVAS_DUE_TIME.end,
        color: c.color || 'yellow',
        category: 'canvas',
        source: 'canvas',
        priority: a.weight === 'Exam' ? 'high' : undefined,
        note: `Canvas due${a.weight ? ' · ' + a.weight : ''} · ${c.name}`,
      });
    });
  });
  notifyEventsChanged();
}
function clearCanvasEvents() { setCanvasEvents([]); }
function useEvents() {
  const [, setTick] = React.useState(0);
  React.useEffect(() => {
    const cb = () => setTick(t => t + 1);
    EVENT_LISTENERS.add(cb);
    return () => EVENT_LISTENERS.delete(cb);
  }, []);
}

const MONTH_NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const DAY_NAMES = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
const DAY_FULL = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];

// Convert HH:MM to minutes
function tm(s) { const [h,m] = s.split(':').map(Number); return h*60+m; }
// Respects the user's 12/24-hour preference (settings.timeFormat), so every
// place that prints an event time updates the instant the setting changes.
function fmtTime(s) {
  const [h,m] = s.split(':').map(Number);
  const fmt = (settingsStore && settingsStore.get().timeFormat) || '12';
  if (fmt === '24') return `${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}`;
  const period = h >= 12 ? 'PM' : 'AM';
  const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
  return `${h12}:${m.toString().padStart(2,'0')} ${period}`;
}
// Format an hour-of-day (0–23) for axis labels, honoring timeFormat.
function fmtHour(h) {
  const fmt = (settingsStore && settingsStore.get().timeFormat) || '12';
  if (fmt === '24') return `${String(h).padStart(2,'0')}:00`;
  return h === 0 ? '12 AM' : h === 12 ? '12 PM' : h > 12 ? `${h-12} PM` : `${h} AM`;
}

// ── Account-sync bridge ───────────────────────────────────────────────────
// Helpers the account-sync engine (auth.jsx) uses to (a) subscribe to data
// mutations so it can debounce-push, and (b) bulk-replace the in-memory stores
// from a server pull. Events are stored by day-offset from "today"; on the
// wire they travel as absolute 'YYYY-MM-DD' dates so they survive across days
// and devices.
function subscribeEvents(cb)     { EVENT_LISTENERS.add(cb);    return () => EVENT_LISTENERS.delete(cb); }
function subscribeCategories(cb) { CATEGORY_LISTENERS.add(cb); return () => CATEGORY_LISTENERS.delete(cb); }
function ymdLocal(d)   { return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; }
function parseYmdLocal(s){ const [y,m,dd] = String(s).split('-').map(Number); return new Date(y, (m||1)-1, dd||1); }

// Serialize every in-memory event to a flat, date-stamped array for the server.
function exportAllEvents() {
  const out = [];
  for (const k of Object.keys(EVENTS_BY_OFFSET)) {
    const date = ymdLocal(dateAtOffset(Number(k)));
    for (const e of EVENTS_BY_OFFSET[k]) {
      out.push({
        date, start: e.start, end: e.end, title: e.title,
        color: e.color ?? null, category: e.category ?? null,
        priority: e.priority ?? null, location: e.location ?? null,
        note: e.note ?? null, source: e.source ?? null,
      });
    }
  }
  return out;
}
// Replace the whole event store from a server pull, mapping absolute dates back
// to the live offset model. Fires one re-render.
function replaceAllEvents(list) {
  for (const k of Object.keys(EVENTS_BY_OFFSET)) delete EVENTS_BY_OFFSET[k];
  (list || []).forEach(e => {
    if (!e || !e.date) return;
    const off = offsetForDate(parseYmdLocal(e.date));
    if (!EVENTS_BY_OFFSET[off]) EVENTS_BY_OFFSET[off] = [];
    EVENTS_BY_OFFSET[off].push({
      title: e.title, start: e.start, end: e.end,
      color: e.color || undefined, category: e.category || undefined,
      priority: e.priority || undefined, location: e.location || undefined,
      note: e.note || undefined, source: e.source || undefined,
    });
  });
  notifyEventsChanged();
}
// Replace the whole category store from a server pull. Fires one re-render.
function replaceAllCategories(list) {
  CATEGORIES.length = 0;
  (list || []).forEach(c => {
    if (!c || !c.id || !c.name) return;
    CATEGORIES.push({ id: String(c.id), name: String(c.name), color: String(c.color || 'blue'), count: 0 });
  });
  notifyCategoriesChanged();
}

Object.assign(window, {
  CHRONO_TODAY, CATEGORIES, EVENTS_BY_OFFSET,
  dateAtOffset, offsetForDate, eventsForOffset,
  addEventAtOffset, updateEventByRef, deleteEventByRef, setEventCategory, useEvents,
  setCanvasEvents, clearCanvasEvents,
  categoriesWithCounts, useCategories, useCategoryList, addCategory, removeCategory,
  usHolidaysForYear, holidayForDate, holidayForOffset,
  useProfile, setProfile, useSettings, setSettings, resetSettings,
  useIntegrations, setIntegrations, eventStats,
  useOnboarding, setOnboarding, resetOnboarding,
  setTutorialView, useTutorialView, setTutorialAI, useTutorialAI,
  MONTH_NAMES, DAY_NAMES, DAY_FULL, tm, fmtTime, fmtHour,
  // Sync bridge
  settingsStore, profileStore,
  subscribeEvents, subscribeCategories,
  exportAllEvents, replaceAllEvents, replaceAllCategories,
});

// Events live in memory (not persisted), but the Canvas connection is persisted
// in localStorage. On load, re-materialize the synced assignment events so a
// connected Canvas still shows up on the calendar after a refresh.
try {
  const ig = integrationsStore.get();
  if (ig.canvas && Array.isArray(ig.canvasCourses) && ig.canvasCourses.length) {
    setCanvasEvents(ig.canvasCourses);
  }
} catch (e) { /* non-fatal */ }
