// VYB LIFE — Store (Supabase-backed)
// Same public API as the previous localStorage store so views don't change.
// State shape is preserved; mutations are optimistic + fire-and-forget to Supabase.

const todayKey = () => {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
};
const dayKey = (offset=0) => {
  const d = new Date();
  d.setDate(d.getDate()+offset);
  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
};
const weekKeys = () => {
  const d = new Date();
  const dow = d.getDay() === 0 ? 7 : d.getDay();
  const mon = new Date(d); mon.setDate(d.getDate() - (dow-1));
  return Array.from({length:7}, (_,i) => {
    const x = new Date(mon); x.setDate(mon.getDate()+i);
    return `${x.getFullYear()}-${String(x.getMonth()+1).padStart(2,'0')}-${String(x.getDate()).padStart(2,'0')}`;
  });
};
const todayDow = () => { const d = new Date().getDay(); return d===0?6:d-1; };

const calcStreak = (checkIns) => {
  let streak = 0;
  const today = todayKey();
  let date = new Date();
  if (!checkIns[today]) date.setDate(date.getDate()-1);
  while (true) {
    const k = `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`;
    if (checkIns[k]) { streak++; date.setDate(date.getDate()-1); } else break;
  }
  return streak;
};

const DEFAULT_STATE = {
  user: { name: '', onboarded: false },
  habits: [],
  habitCategories: [],
  affirmations: [],
  workouts: {},
  reading: { id: null, title: '', author: '', currentPage: 0, totalPages: 0, notes: [] },
  books: [],
  ideas: [],
  tasks: [],
  mood: {},
  notes: '',
  readingGoals: { yearlyTarget: 12, monthlyTarget: 1 },
};

const DEFAULT_CATEGORIES = [
  { name: 'Health',          icon: 'heart',     color: 'sage'  },
  { name: 'Personal Growth', icon: 'sunrise',   color: 'dust'  },
  { name: 'Finance',         icon: 'wallet',    color: 'sand'  },
  { name: 'Focus',           icon: 'target',    color: 'slate' },
  { name: 'Creativity',      icon: 'sparkles',  color: 'clay'  },
  { name: 'Lifestyle',       icon: 'leaf',      color: 'sage'  },
  { name: 'Business',        icon: 'briefcase', color: 'sand'  },
];

const DEFAULT_AFFIRMATIONS = [
  'I am capable of achieving what I set my mind to.',
  'Every day, in every way, I am getting better.',
  'I accept challenges as opportunities.',
  'Small, consistent actions compound into greatness.',
  'Discipline is the bridge between goals and accomplishment.',
  'I am the architect of my future self.',
  'Today\u2019s effort is tomorrow\u2019s identity.',
  'I show up, especially when it is hard.',
];

// Shape DB rows into the in-memory tree the views expect.
const shapeBook = (b, entries=[]) => ({
  id: b.id,
  title: b.title || '',
  author: b.author || '',
  currentPage: b.current_page || 0,
  totalPages: b.total_pages || 0,
  status: b.status || 'reading',
  coverUrl: b.cover_url || '',
  isbn: b.isbn || '',
  description: b.description || '',
  publisher: b.publisher || '',
  publishedDate: b.published_date || '',
  externalSource: b.external_source || '',
  externalId: b.external_id || '',
  finishedAt: b.finished_at || null,
  createdAt: (b.created_at || '').slice(0,10),
  updatedAt: (b.updated_at || b.created_at || '').slice(0,10),
  entries: entries.map(n => ({
    id: n.id,
    text: n.text,
    kind: n.kind || 'note',
    page: n.page ?? null,
    date: n.day,
    createdAt: n.created_at,
  })),
});

const hydrate = ({ profile, habits, checkins, workouts, allBooks, allEntries, ideas, tasks, moods, quickNotes, readingGoals, habitCategories, affirmations }) => {
  const habitMap = Object.fromEntries(habits.map(h => {
    const customDays = Array.isArray(h.custom_days) ? h.custom_days : [];
    const frequency  = h.frequency || 'daily';
    // Default weekly target: 7 for daily habits, customDays.length for custom.
    const weeklyTarget = (h.weekly_target == null)
      ? (frequency === 'custom' ? customDays.length : 7)
      : h.weekly_target;
    return [h.id, {
      id:h.id, name:h.name, icon:h.icon||'target',
      categoryId: h.category_id ?? null,
      frequency, customDays, weeklyTarget,
      createdAt:(h.created_at||'').slice(0,10), checkIns:{},
    }];
  }));
  for (const c of checkins) {
    const h = habitMap[c.habit_id]; if (!h) continue;
    h.checkIns[c.day] = true;
  }
  const workoutMap = {};
  for (const w of workouts) workoutMap[w.day] = { type:w.type||'', intensity:w.intensity||'', duration:w.duration||0, notes:w.notes||'', done:!!w.done };
  const moodMap = {};
  for (const m of moods) moodMap[m.day] = { mood:m.mood, energy:m.energy };
  const entriesByBook = {};
  for (const e of allEntries) {
    (entriesByBook[e.book_id] ||= []).push(e);
  }
  const books = allBooks.map(b => shapeBook(b, entriesByBook[b.id] || []));
  const active = books.find(b => b.status === 'reading') || null;
  return {
    user: { name: profile?.name || '', onboarded: !!profile?.name, identityStatement: profile?.identity_statement || '' },
    habits: Object.values(habitMap),
    habitCategories: (habitCategories || []).map(c => ({
      id: c.id, name: c.name, icon: c.icon || 'target',
      color: c.color || 'sand', sortOrder: c.sort_order ?? 0,
      active: c.active !== false,
    })).sort((a,b) => a.sortOrder - b.sortOrder),
    affirmations: (affirmations || []).filter(a => !a.archived).map(a => ({
      id: a.id, text: a.text, isDefault: !!a.is_default,
    })),
    workouts: workoutMap,
    books,
    reading: active
      ? { id:active.id, title:active.title, author:active.author, currentPage:active.currentPage, totalPages:active.totalPages,
          notes: active.entries.map(n => ({ id:n.id, text:n.text, date:n.date })) }
      : { ...DEFAULT_STATE.reading },
    ideas: ideas.map(i => ({ id:i.id, text:i.text, tag:i.tag||'Video', archived:!!i.archived, createdAt:(i.created_at||'').slice(0,10) })),
    tasks: tasks.map(t => ({
      id:t.id, text:t.text, pri:t.pri||'important', done:!!t.done,
      project: t.project || null,
      sortOrder: t.sort_order ?? 0,
      completedAt: t.completed_at || null,
      createdAt:(t.created_at||'').slice(0,10),
    })),
    mood: moodMap,
    notes: quickNotes?.text || '',
    readingGoals: {
      yearlyTarget: readingGoals?.yearly_target ?? 12,
      monthlyTarget: readingGoals?.monthly_target ?? 1,
    },
  };
};

const fetchAll = async (sb, userId) => {
  const [profile, habits, checkins, workouts, books, entries, ideas, tasks, moods, quickNotes, readingGoals, habitCategories, affirmations] = await Promise.all([
    sb.from('profiles').select('*').eq('id', userId).maybeSingle(),
    sb.from('habits').select('*').order('created_at'),
    sb.from('habit_checkins').select('*'),
    sb.from('workouts').select('*'),
    sb.from('reading_books').select('*').order('updated_at', { ascending:false, nullsFirst:false }),
    sb.from('reading_notes').select('*').order('created_at', { ascending:false }),
    sb.from('content_ideas').select('*').order('created_at', { ascending:false }),
    sb.from('tasks').select('*').order('sort_order').order('created_at'),
    sb.from('mood_logs').select('*'),
    sb.from('quick_notes').select('*').eq('user_id', userId).maybeSingle(),
    sb.from('reading_goals').select('*').eq('user_id', userId).maybeSingle(),
    sb.from('habit_categories').select('*').order('sort_order'),
    sb.from('affirmations').select('*').order('created_at'),
  ]);
  return hydrate({
    profile: profile.data,
    habits: habits.data || [],
    checkins: checkins.data || [],
    workouts: workouts.data || [],
    allBooks: books.data || [],
    allEntries: entries.data || [],
    ideas: ideas.data || [],
    tasks: tasks.data || [],
    moods: moods.data || [],
    quickNotes: quickNotes.data,
    readingGoals: readingGoals.data,
    habitCategories: habitCategories.data || [],
    affirmations: affirmations.data || [],
  });
};

// Ensure an active reading_books row exists for the user, return its id.
const ensureActiveBook = async (sb, userId, current) => {
  if (current && current.id) return current.id;
  const { data, error } = await sb.from('reading_books')
    .insert({ user_id: userId, is_active: true, title: current?.title || '', author: current?.author || '',
              current_page: current?.currentPage || 0, total_pages: current?.totalPages || 0 })
    .select().single();
  if (error) throw error;
  return data.id;
};

const useStore = (session) => {
  const userId = session?.user?.id;
  const sb = window.initSupabase();
  const [state, setState] = React.useState(DEFAULT_STATE);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    if (!userId) return;
    let cancelled = false;
    (async () => {
      setLoading(true);
      try {
        const next = await fetchAll(sb, userId);
        if (!cancelled) setState(next);
      } catch (e) { console.error('[store] load failed', e); }
      finally { if (!cancelled) setLoading(false); }
    })();
    return () => { cancelled = true; };
  }, [userId]);

  const bg = (p) => { Promise.resolve(p).catch(e => console.error('[store] write failed', e)); };

  // ── User ──────────────────────────────────────────────
  const setName = (name) => {
    setState(s => ({...s, user: {...s.user, name, onboarded:true}}));
    bg(sb.from('profiles').upsert({ id: userId, name }).then());
  };
  const setIdentityStatement = (text) => {
    setState(s => ({...s, user: {...s.user, identityStatement: text}}));
    bg(sb.from('profiles').upsert({ id: userId, identity_statement: text }).then());
  };

  // ── Habit Categories ──────────────────────────────────
  const addHabitCategory = async ({ name, icon='target', color='sand', sortOrder } = {}) => {
    if (!name || !name.trim()) return null;
    const so = typeof sortOrder === 'number' ? sortOrder
      : (state.habitCategories.reduce((m,c) => Math.max(m, c.sortOrder), -10) + 10);
    const { data, error } = await sb.from('habit_categories')
      .insert({ user_id: userId, name: name.trim(), icon, color, sort_order: so })
      .select().single();
    if (error) { console.error('[store] addHabitCategory', error); return null; }
    const cat = { id: data.id, name: data.name, icon: data.icon, color: data.color, sortOrder: data.sort_order };
    setState(s => ({ ...s, habitCategories: [...s.habitCategories, cat].sort((a,b)=>a.sortOrder-b.sortOrder) }));
    return cat;
  };
  const updateHabitCategory = (id, patch) => {
    setState(s => ({ ...s, habitCategories: s.habitCategories.map(c => c.id===id ? { ...c, ...patch } : c).sort((a,b)=>a.sortOrder-b.sortOrder) }));
    const dbPatch = {};
    if ('name' in patch) dbPatch.name = patch.name;
    if ('icon' in patch) dbPatch.icon = patch.icon;
    if ('color' in patch) dbPatch.color = patch.color;
    if ('sortOrder' in patch) dbPatch.sort_order = patch.sortOrder;
    if ('active' in patch) dbPatch.active = !!patch.active;
    if (Object.keys(dbPatch).length) bg(sb.from('habit_categories').update(dbPatch).eq('id', id).then());
  };
  const deleteHabitCategory = (id) => {
    setState(s => ({
      ...s,
      habitCategories: s.habitCategories.filter(c => c.id !== id),
      habits: s.habits.map(h => h.categoryId === id ? { ...h, categoryId: null } : h),
    }));
    bg(sb.from('habit_categories').delete().eq('id', id).then());
  };
  const reorderHabitCategories = (orderedIds) => {
    const stride = 10;
    const idToOrder = Object.fromEntries(orderedIds.map((id, i) => [id, i * stride]));
    setState(s => ({
      ...s,
      habitCategories: s.habitCategories
        .map(c => idToOrder[c.id] != null ? { ...c, sortOrder: idToOrder[c.id] } : c)
        .sort((a,b)=>a.sortOrder-b.sortOrder),
    }));
    for (const id of orderedIds) {
      bg(sb.from('habit_categories').update({ sort_order: idToOrder[id] }).eq('id', id).then());
    }
  };

  // Auto-seed: 5 starter categories + affirmations on first load if empty.
  // Also assigns any uncategorized habits to a "My Practice" fallback.
  React.useEffect(() => {
    if (!userId || loading) return;
    (async () => {
      try {
        // Rename any legacy "My Practice" category to "Uncategorized" so it sorts last
        // and no longer shows the old wording.
        const legacy = state.habitCategories.find(c => c.name === 'My Practice');
        if (legacy) {
          bg(sb.from('habit_categories').update({ name: 'Uncategorized', sort_order: 9999 }).eq('id', legacy.id).then());
          setState(s => ({ ...s, habitCategories: s.habitCategories.map(c => c.id===legacy.id ? { ...c, name:'Uncategorized', sortOrder: 9999 } : c).sort((a,b)=>a.sortOrder-b.sortOrder) }));
        }

        // Insert any DEFAULT_CATEGORIES that don't yet exist for this user.
        const existingNames = new Set(state.habitCategories.map(c => c.name));
        const missing = DEFAULT_CATEGORIES.map((c,i)=>({...c,i})).filter(c => !existingNames.has(c.name));
        if (missing.length) {
          const rows = missing.map(c => ({
            user_id: userId, name: c.name, icon: c.icon, color: c.color, sort_order: c.i * 10,
            active: false, // User intentionally opts in via the Choose Areas step.
          }));
          const { data } = await sb.from('habit_categories').insert(rows).select();
          if (data && data.length) {
            const added = data.map(c => ({ id: c.id, name: c.name, icon: c.icon, color: c.color, sortOrder: c.sort_order, active: c.active !== false }));
            setState(s => ({ ...s, habitCategories: [...s.habitCategories, ...added].sort((a,b)=>a.sortOrder-b.sortOrder) }));
          }
        }

        // Re-assign orphans to the Health area (or first available default).
        const orphanHabits = state.habits.filter(h => h.categoryId == null);
        if (orphanHabits.length > 0) {
          const fallback = state.habitCategories.find(c => c.name === 'Health')
            || state.habitCategories.find(c => DEFAULT_CATEGORIES.some(d => d.name === c.name));
          if (fallback) {
            const ids = orphanHabits.map(h => h.id);
            bg(sb.from('habits').update({ category_id: fallback.id }).in('id', ids).then());
            setState(s => ({ ...s, habits: s.habits.map(h => h.categoryId == null ? { ...h, categoryId: fallback.id } : h) }));
          }
        }
      } catch (e) { console.error('[store] habits seed', e); }
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [userId, loading]);

  // ── Habits ────────────────────────────────────────────
  const addHabit = async (nameOrObj, iconArg='target') => {
    const p = typeof nameOrObj === 'string'
      ? { name: nameOrObj, icon: iconArg, categoryId: null, frequency: 'daily' }
      : nameOrObj || {};
    const name = (p.name || '').trim();
    if (!name) return;
    const frequency  = p.frequency || 'daily';
    const customDays = Array.isArray(p.customDays) ? p.customDays : [];
    const weeklyTarget = (p.weeklyTarget != null)
      ? p.weeklyTarget
      : (frequency === 'custom' ? customDays.length : 7);
    const row = {
      user_id: userId,
      name,
      icon: p.icon || 'target',
      category_id: p.categoryId ?? null,
      frequency,
      custom_days: customDays,
      weekly_target: weeklyTarget,
    };
    const { data, error } = await sb.from('habits').insert(row).select().single();
    if (error) { console.error('[store] addHabit', error); return null; }
    // BUGFIX: data.created_at is a UTC ISO string. Slicing it (0,10) yields
    // the UTC date, which can be tomorrow vs the user's LOCAL today (e.g.
    // user creates a habit at 8pm UTC-6 → UTC slice is +1 day). That makes
    // isHabitScheduledOn() filter the new habit out of Today and surface it
    // as an "Extra". Use the user's local today-key for the optimistic state
    // so the new habit appears in Today immediately.
    const _now = new Date();
    const _p = (n) => String(n).padStart(2,'0');
    const _localTodayKey = `${_now.getFullYear()}-${_p(_now.getMonth()+1)}-${_p(_now.getDate())}`;
    setState(s => ({...s, habits: [...s.habits, {
      id: data.id, name: data.name, icon: data.icon || 'target',
      categoryId: data.category_id ?? null,
      frequency: data.frequency || 'daily',
      customDays: Array.isArray(data.custom_days) ? data.custom_days : [],
      weeklyTarget: data.weekly_target == null
        ? ((data.frequency || 'daily') === 'custom' ? (data.custom_days || []).length : 7)
        : data.weekly_target,
      createdAt: _localTodayKey, checkIns: {},
    }]}));
    return data.id;
  };
  const updateHabit = (id, patch) => {
    // If frequency or customDays changes, recompute weeklyTarget unless caller
    // explicitly provided one. Keeps UX simple — user picks days, target follows.
    setState(s => ({...s, habits: s.habits.map(h => {
      if (h.id !== id) return h;
      const next = { ...h, ...patch };
      if (!('weeklyTarget' in patch)) {
        if ('frequency' in patch || 'customDays' in patch) {
          next.weeklyTarget = next.frequency === 'custom'
            ? (Array.isArray(next.customDays) ? next.customDays.length : 0)
            : 7;
        }
      }
      return next;
    })}));
    const dbPatch = {};
    if ('name' in patch) dbPatch.name = patch.name;
    if ('icon' in patch) dbPatch.icon = patch.icon;
    if ('categoryId' in patch) dbPatch.category_id = patch.categoryId ?? null;
    if ('frequency' in patch) dbPatch.frequency = patch.frequency;
    if ('customDays' in patch) dbPatch.custom_days = Array.isArray(patch.customDays) ? patch.customDays : [];
    if ('weeklyTarget' in patch) {
      dbPatch.weekly_target = patch.weeklyTarget;
    } else if ('frequency' in patch || 'customDays' in patch) {
      const freq = ('frequency' in patch) ? patch.frequency : null;
      const cd   = ('customDays' in patch) ? patch.customDays : null;
      // Re-derive from final values (re-read from state would be safer, but
      // patch-derived is good enough for the common flow).
      if (freq === 'custom' && Array.isArray(cd)) dbPatch.weekly_target = cd.length;
      else if (freq === 'daily') dbPatch.weekly_target = 7;
    }
    if (Object.keys(dbPatch).length) bg(sb.from('habits').update(dbPatch).eq('id', id).then());
  };
  const deleteHabit = (id) => {
    setState(s => ({...s, habits: s.habits.filter(h=>h.id!==id)}));
    bg(sb.from('habits').delete().eq('id', id).then());
  };
  const toggleHabit = (id, dayK) => {
    let willAdd = false;
    setState(s => ({...s, habits: s.habits.map(h => {
      if (h.id !== id) return h;
      const ci = {...h.checkIns};
      if (ci[dayK]) { delete ci[dayK]; willAdd = false; }
      else          { ci[dayK] = true; willAdd = true; }
      return {...h, checkIns: ci};
    })}));
    if (willAdd) bg(sb.from('habit_checkins').insert({ user_id:userId, habit_id:id, day:dayK }).then());
    else         bg(sb.from('habit_checkins').delete().eq('habit_id', id).eq('day', dayK).then());
  };

  // ── Workouts ──────────────────────────────────────────
  const setWorkout = (dayK, data) => {
    setState(s => {
      const next = {...(s.workouts[dayK]||{}), ...data};
      const dbRow = { user_id:userId, day:dayK,
        type: next.type ?? null, intensity: next.intensity ?? null,
        duration: next.duration ?? null, notes: next.notes ?? null, done: !!next.done };
      bg(sb.from('workouts').upsert(dbRow, { onConflict:'user_id,day' }).then());
      return {...s, workouts: {...s.workouts, [dayK]: next}};
    });
  };
  const deleteWorkout = (dayK) => {
    setState(s => { const w = {...s.workouts}; delete w[dayK]; return {...s, workouts: w}; });
    bg(sb.from('workouts').delete().eq('user_id', userId).eq('day', dayK).then());
  };

  // ── Reading library ───────────────────────────────────
  const deriveReading = (books) => {
    const a = books.find(b => b.status === 'reading');
    if (!a) return { ...DEFAULT_STATE.reading };
    return { id:a.id, title:a.title, author:a.author, currentPage:a.currentPage, totalPages:a.totalPages,
             notes: a.entries.map(n => ({ id:n.id, text:n.text, date:n.date })) };
  };
  const setBooksAndReading = (s, books) => ({ ...s, books, reading: deriveReading(books) });

  const addBook = async (p={}) => {
    const { title='', author='', totalPages=0, currentPage=0, status='reading',
            coverUrl=null, isbn=null, description=null, publisher=null, publishedDate=null,
            externalSource=null, externalId=null, finishedAt='' } = p;
    let finishedIso = null;
    if (status === 'finished') {
      finishedIso = finishedAt ? new Date(`${finishedAt}T12:00:00`).toISOString() : new Date().toISOString();
    }
    const { data, error } = await sb.from('reading_books').insert({
      user_id:userId, title, author,
      total_pages: parseInt(totalPages)||0, current_page: parseInt(currentPage)||0,
      status, is_active: status==='reading',
      cover_url: coverUrl || null, isbn: isbn || null, description: description || null,
      publisher: publisher || null, published_date: publishedDate || null,
      external_source: externalSource || null, external_id: externalId || null,
      finished_at: finishedIso,
    }).select().single();
    if (error) { console.error('[store] addBook', error); return null; }
    const book = shapeBook(data, []);
    setState(s => setBooksAndReading(s, [book, ...s.books]));
    return book;
  };

  const updateBook = (id, patch) => {
    const nowIso = new Date().toISOString();
    // Normalize an explicit finishedAt patch ('YYYY-MM-DD' | '' | null) → ISO | null
    const hasFinishedAtPatch = 'finishedAt' in patch;
    const explicitFinishedIso = hasFinishedAtPatch
      ? (patch.finishedAt ? new Date(`${patch.finishedAt}T12:00:00`).toISOString() : null)
      : undefined;
    setState(s => {
      const target = s.books.find(b => b.id===id);
      if (!target) return s;
      // Progress edit (currentPage changed without explicit status): bump to top, force reading
      // unless the book is already finished (finished books stay finished).
      const progressOnly = 'currentPage' in patch && !('status' in patch);
      const merged = { ...target, ...patch, updatedAt: todayKey() };
      let bumpToFront = false;
      if (progressOnly) {
        const total = merged.totalPages || 0;
        const cp = merged.currentPage || 0;
        if (target.status === 'finished') {
          if (total > 0 && cp < total) { merged.status = 'reading'; bumpToFront = true; merged.finishedAt = null; }
        } else {
          merged.status = 'reading';
          bumpToFront = true;
        }
      }
      if ('status' in patch) {
        if (patch.status === 'finished') {
          merged.finishedAt = hasFinishedAtPatch
            ? (explicitFinishedIso || nowIso)
            : (target.finishedAt || nowIso);
        } else if (target.status === 'finished') {
          merged.finishedAt = null;
        }
      } else if (hasFinishedAtPatch) {
        merged.finishedAt = merged.status === 'finished' ? (explicitFinishedIso || nowIso) : null;
      }
      const rest = s.books.filter(b => b.id!==id);
      const books = bumpToFront ? [merged, ...rest] : s.books.map(b => b.id===id ? merged : b);
      return setBooksAndReading(s, books);
    });
    const dbPatch = { updated_at: new Date().toISOString() };
    if ('title' in patch) dbPatch.title = patch.title;
    if ('author' in patch) dbPatch.author = patch.author;
    if ('currentPage' in patch) dbPatch.current_page = parseInt(patch.currentPage)||0;
    if ('totalPages' in patch) dbPatch.total_pages = parseInt(patch.totalPages)||0;
    if ('coverUrl' in patch) dbPatch.cover_url = patch.coverUrl || null;
    if ('isbn' in patch) dbPatch.isbn = patch.isbn || null;
    if ('description' in patch) dbPatch.description = patch.description || null;
    if ('publisher' in patch) dbPatch.publisher = patch.publisher || null;
    if ('publishedDate' in patch) dbPatch.published_date = patch.publishedDate || null;
    if ('externalSource' in patch) dbPatch.external_source = patch.externalSource || null;
    if ('externalId' in patch) dbPatch.external_id = patch.externalId || null;
    const t = state.books.find(b => b.id===id);
    if ('status' in patch) {
      dbPatch.status = patch.status;
      dbPatch.is_active = patch.status==='reading';
      if (t) {
        if (patch.status === 'finished') {
          dbPatch.finished_at = hasFinishedAtPatch
            ? (explicitFinishedIso || nowIso)
            : (t.finishedAt || nowIso);
        } else if (t.status === 'finished') {
          dbPatch.finished_at = null;
        }
      }
    } else if (hasFinishedAtPatch) {
      if (t && t.status === 'finished') dbPatch.finished_at = explicitFinishedIso || nowIso;
      else dbPatch.finished_at = null;
    } else if ('currentPage' in patch) {
      // Progress-only update: persist status='reading' for non-reading books,
      // and revert finished→reading when user drops below totalPages.
      if (t) {
        const newCp = parseInt(patch.currentPage)||0;
        const total = t.totalPages || 0;
        if (t.status === 'finished') {
          if (total > 0 && newCp < total) { dbPatch.status = 'reading'; dbPatch.is_active = true; dbPatch.finished_at = null; }
        } else if (t.status !== 'reading') {
          dbPatch.status = 'reading'; dbPatch.is_active = true;
        }
      }
    }
    bg(sb.from('reading_books').update(dbPatch).eq('id', id).then());
  };

  const deleteBook = (id) => {
    setState(s => setBooksAndReading(s, s.books.filter(b => b.id!==id)));
    bg(sb.from('reading_books').delete().eq('id', id).then());
  };

  const finishBook = (id) => {
    const b = state.books.find(x => x.id === id);
    const patch = { status: 'finished' };
    if (b && b.totalPages > 0) patch.currentPage = b.totalPages;
    return updateBook(id, patch);
  };

  const addBookEntry = async (bookId, { text, kind='note', page=null }) => {
    if (!text.trim()) return null;
    const day = todayKey();
    const { data, error } = await sb.from('reading_notes')
      .insert({ user_id:userId, book_id:bookId, text:text.trim(), kind, page: page ?? null, day }).select().single();
    if (error) { console.error('[store] addBookEntry', error); return null; }
    const entry = { id:data.id, text:data.text, kind:data.kind||'note', page:data.page??null, date:data.day, createdAt:data.created_at };
    setState(s => {
      const books = s.books.map(b => b.id===bookId ? {...b, entries:[entry, ...b.entries]} : b);
      return setBooksAndReading(s, books);
    });
    return entry;
  };

  const deleteBookEntry = (bookId, entryId) => {
    setState(s => {
      const books = s.books.map(b => b.id===bookId ? {...b, entries: b.entries.filter(e => e.id!==entryId)} : b);
      return setBooksAndReading(s, books);
    });
    bg(sb.from('reading_notes').delete().eq('id', entryId).then());
  };

  // Back-compat (used by overview ReadingWidget and any older callers)
  const setReading = async (patch) => {
    const active = state.books.find(b => b.status === 'reading');
    if (active) return updateBook(active.id, patch);
    if (!patch.title && patch.currentPage == null && patch.totalPages == null) return;
    return addBook({ title:patch.title||'', author:patch.author||'',
      currentPage: patch.currentPage||0, totalPages: patch.totalPages||0, status:'reading' });
  };
  const addReadingNote = async (text) => {
    const active = state.books.find(b => b.status === 'reading');
    const id = active ? active.id : (await addBook({ status:'reading' }))?.id;
    if (!id) return;
    return addBookEntry(id, { text, kind:'insight' });
  };
  const deleteReadingNote = (entryId) => {
    const book = state.books.find(b => b.entries.some(e => e.id===entryId));
    if (book) deleteBookEntry(book.id, entryId);
  };

  // ── Reading goals ─────────────────────────────────────
  const setReadingGoal = (patch) => {
    setState(s => ({ ...s, readingGoals: { ...s.readingGoals, ...patch } }));
    const dbPatch = { user_id: userId, updated_at: new Date().toISOString() };
    if ('yearlyTarget' in patch) dbPatch.yearly_target = Math.max(0, parseInt(patch.yearlyTarget)||0);
    if ('monthlyTarget' in patch) dbPatch.monthly_target = Math.max(0, parseInt(patch.monthlyTarget)||0);
    bg(sb.from('reading_goals').upsert(dbPatch, { onConflict:'user_id' }).then());
  };

  // ── Ideas ─────────────────────────────────────────────
  const addIdea = async (text, tag=null) => {
    const { data, error } = await sb.from('content_ideas')
      .insert({ user_id:userId, text, tag, archived:false }).select().single();
    if (error) { console.error(error); return null; }
    setState(s => ({...s, ideas: [{ id:data.id, text:data.text, tag:data.tag, archived:false, createdAt:(data.created_at||'').slice(0,10) }, ...s.ideas]}));
    return data.id;
  };
  const updateIdea = (id, patch) => {
    setState(s => ({...s, ideas: s.ideas.map(i => i.id===id ? {...i, ...patch} : i)}));
    const dbPatch = {};
    if ('text' in patch) dbPatch.text = patch.text;
    if ('tag' in patch) dbPatch.tag = patch.tag;
    if ('archived' in patch) dbPatch.archived = patch.archived;
    if (Object.keys(dbPatch).length) bg(sb.from('content_ideas').update(dbPatch).eq('id', id).then());
  };
  const deleteIdea = (id) => {
    setState(s => ({...s, ideas: s.ideas.filter(i=>i.id!==id)}));
    bg(sb.from('content_ideas').delete().eq('id', id).then());
  };

  // ── Tasks ─────────────────────────────────────────────
  const addTask = async (textOrObj, priArg='important') => {
    const p = typeof textOrObj === 'string'
      ? { text: textOrObj, pri: priArg, project: null }
      : (textOrObj || {});
    const text = (p.text || '').trim();
    if (!text) return null;
    const project = p.project ? String(p.project).trim() || null : null;
    // New tasks land at the bottom of their (pri, project) group.
    const peers = state.tasks.filter(t => t.pri === (p.pri || 'important') && (t.project || null) === project);
    const so = peers.reduce((m,t) => Math.max(m, t.sortOrder || 0), -10) + 10;
    const insertPayload = { user_id:userId, text, pri: p.pri || 'important', done:false, project, sort_order: so };
    const { data, error } = await sb.from('tasks')
      .insert(insertPayload)
      .select().single();
    if (error || !data) {
      // Surface the full payload + error so the cause (RLS, schema drift,
      // null user_id, invalid pri value) is debuggable in the console
      // instead of the task silently disappearing from the UI.
      console.error('[store.addTask] insert failed', { error, payload: insertPayload });
      return null;
    }
    setState(s => ({...s, tasks: [...s.tasks, {
      id:data.id, text:data.text, pri:data.pri, done:false,
      project: data.project || null, sortOrder: data.sort_order ?? 0,
      completedAt: data.completed_at || null,
      createdAt:(data.created_at||'').slice(0,10),
    }]}));
    return data.id;
  };
  const updateTask = (id, patch) => {
    setState(s => ({...s, tasks: s.tasks.map(t => t.id===id ? {...t, ...patch} : t)}));
    const dbPatch = {};
    if ('text' in patch) dbPatch.text = patch.text;
    if ('pri' in patch) dbPatch.pri = patch.pri;
    if ('done' in patch) dbPatch.done = patch.done;
    if ('project' in patch) dbPatch.project = patch.project ? String(patch.project).trim() || null : null;
    if ('sortOrder' in patch) dbPatch.sort_order = patch.sortOrder;
    if ('completedAt' in patch) dbPatch.completed_at = patch.completedAt;
    if (Object.keys(dbPatch).length) bg(sb.from('tasks').update(dbPatch).eq('id', id).then());
  };
  const toggleTask = (id) => {
    let nextDone = false;
    let nextCompletedAt = null;
    setState(s => ({...s, tasks: s.tasks.map(t => {
      if (t.id !== id) return t;
      nextDone = !t.done;
      nextCompletedAt = nextDone ? new Date().toISOString() : null;
      return {...t, done: nextDone, completedAt: nextCompletedAt};
    })}));
    bg(sb.from('tasks').update({ done: nextDone, completed_at: nextCompletedAt }).eq('id', id).then());
  };
  const deleteTask = (id) => {
    setState(s => ({...s, tasks: s.tasks.filter(t=>t.id!==id)}));
    bg(sb.from('tasks').delete().eq('id', id).then());
  };
  const reorderTasks = (orderedIds) => {
    const stride = 10;
    const idToOrder = Object.fromEntries(orderedIds.map((id, i) => [id, i * stride]));
    setState(s => ({
      ...s,
      tasks: s.tasks.map(t => idToOrder[t.id] != null ? { ...t, sortOrder: idToOrder[t.id] } : t),
    }));
    for (const id of orderedIds) {
      bg(sb.from('tasks').update({ sort_order: idToOrder[id] }).eq('id', id).then());
    }
  };

  // ── Mood ──────────────────────────────────────────────
  const setMoodDay = (dayK, patch) => {
    setState(s => {
      const next = {...(s.mood[dayK]||{}), ...patch};
      bg(sb.from('mood_logs').upsert({ user_id:userId, day:dayK, mood: next.mood ?? null, energy: next.energy ?? null }, { onConflict:'user_id,day' }).then());
      return {...s, mood: {...s.mood, [dayK]: next}};
    });
  };

  // ── Quick notes ───────────────────────────────────────
  let notesTimer = null;
  const setNotes = (text) => {
    setState(s => ({...s, notes: text}));
    if (notesTimer) clearTimeout(notesTimer);
    notesTimer = setTimeout(() => {
      bg(sb.from('quick_notes').upsert({ user_id:userId, text, updated_at: new Date().toISOString() }, { onConflict:'user_id' }).then());
    }, 400);
  };

  // ── Sign out ──────────────────────────────────────────
  const signOut = async () => { await sb.auth.signOut(); };

  // ── Reload all data from server ───────────────────────
  const reload = async () => {
    if (!userId) return;
    setLoading(true);
    try {
      const next = await fetchAll(sb, userId);
      setState(next);
    } catch (e) { console.error('[store] reload failed', e); }
    finally { setLoading(false); }
  };

  // ── Developer: reset all data for the current user ────
  // Deletes only rows scoped to this user_id. Auth user is preserved.
  // Returns { ok, errors: [{table, error}] }.
  // Tables to delete per resettable category. Order matters: children before parents.
  // Missing tables (42P01) are treated as non-fatal so the reset still succeeds.
  const RESET_CATEGORIES = {
    habits:   ['habit_checkins', 'habits', 'habit_categories'],
    tasks:    ['tasks'],
    ideas:    ['content_ideas'],
    reading:  ['reading_notes', 'reading_books', 'reading_goals'],
    mood:     ['mood_logs'],
    focus:    ['focus_sessions', 'focus_logs'],
    workouts: ['workouts'],
    notes:    ['quick_notes', 'affirmations'],
  };

  const resetAccountData = async (categories) => {
    if (!userId) return { ok: false, errors: [{ table: 'auth', error: 'No user' }] };
    // Back-compat: undefined/null = reset everything (matches prior behavior).
    const keys = Array.isArray(categories) && categories.length
      ? categories
      : Object.keys(RESET_CATEGORIES);

    // Build ordered delete list (children first), de-duplicated.
    const seen = new Set();
    const ordered = [];
    for (const k of keys) {
      const tables = RESET_CATEGORIES[k] || [];
      for (const t of tables) if (!seen.has(t)) { seen.add(t); ordered.push({ category: k, table: t }); }
    }

    const errors = [];
    const succeeded = new Set();
    for (const { category, table } of ordered) {
      const { error } = await sb.from(table).delete().eq('user_id', userId);
      if (error) {
        if (error.code === '42P01' || /does not exist/i.test(error.message || '')) {
          console.warn(`[reset] table ${table} missing — skipping`);
          continue;
        }
        console.error(`[reset] delete ${table}`, error);
        errors.push({ category, table, error: error.message });
      } else {
        succeeded.add(category);
      }
    }
    // Categories that had no failed table delete are considered reset.
    const failedCats = new Set(errors.map(e => e.category));
    const resetCats = keys.filter(k => !failedCats.has(k));
    if (resetCats.length) await reload();
    return { ok: errors.length === 0, errors, resetCategories: resetCats };
  };

  return {
    state, loading, setName, setIdentityStatement, signOut, reload, resetAccountData,
    addHabit, updateHabit, deleteHabit, toggleHabit,
    addHabitCategory, updateHabitCategory, deleteHabitCategory, reorderHabitCategories,
    setWorkout, deleteWorkout,
    setReading, addReadingNote, deleteReadingNote,
    addBook, updateBook, deleteBook, finishBook, addBookEntry, deleteBookEntry, setReadingGoal,
    addIdea, updateIdea, deleteIdea,
    addTask, updateTask, toggleTask, deleteTask, reorderTasks,
    setMoodDay, setNotes,
  };
};

Object.assign(window, { useStore, todayKey, dayKey, weekKeys, todayDow, calcStreak });
