// VYB LIFE — Momentum System (Step 1: pure calculation helper)
//
// Pure, side-effect-free function that derives daily/weekly momentum metrics
// from existing app data. Inputs are read-only; nothing is mutated, fetched,
// or persisted. Safe to call with empty / undefined inputs — every field
// falls back to a stable default so UI consumers never crash.
//
// Data shape assumptions (match dashboard-store.jsx):
//   habits[]       → { id, frequency:'daily'|'custom', customDays:[0..6 (Sun=0)],
//                      checkIns: { 'YYYY-MM-DD': true } }
//   tasks[]        → { id, done, completedAt, pri:'urgent'|'important'|'later' }
//   ideas[]        → { id, archived, createdAt: 'YYYY-MM-DD' }
//   books[]        → { status, currentPage, totalPages, entries:[{ date:'YYYY-MM-DD' }] }
//   readingEntries → optional flat override; if absent, derived from books[].entries
//   focusSessions  → optional; Focus persistence does not exist yet (returns 0)
//
// All date math uses local date boundaries (matches todayKey() in store).

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

  // Returns true if a given habit is scheduled to run on `date`.
  // - frequency 'daily' (or missing) → every day
  // - frequency 'custom' + customDays → only on listed weekdays (0=Sun..6=Sat)
  // - frequency 'weekly_count' → TODO: not auto-scheduled per day; treated as
  //   not-scheduled for daily progress (Weekly Consistency surface owns it).
  // Habits created AFTER `date` are not scheduled retroactively.
  const isHabitScheduledOn = (habit, date) => {
    if (!habit) return false;
    if (habit.createdAt) {
      const c = String(habit.createdAt).slice(0, 10);
      const dk = dKey(date);
      if (dk < c) {
        // BUGFIX: createdAt may be a UTC date slice while `date` is local.
        // Allow a 1-day grace so a habit created late in local time (which
        // becomes "tomorrow" in UTC) still shows up in today's schedule.
        const cd = new Date(c + 'T00:00:00');
        const dd = new Date(dk + 'T00:00:00');
        if ((cd.getTime() - dd.getTime()) > 24 * 60 * 60 * 1000) return false;
      }
    }
    const f = habit.frequency || 'daily';
    if (f === 'daily') return true;
    if (f === 'custom') {
      const days = Array.isArray(habit.customDays) ? habit.customDays : [];
      return days.includes(date.getDay());
    }
    // 'weekly_count' or unknown → not scheduled per-day for MVP.
    return false;
  };

  // Status labels reflect HABITS only (not tasks/ideas/reading/focus).
  // Tasks, ideas, reading, and focus live under "Activity Today" — they do
  // not influence the daily-habits status or progress bar.
  // Status reflects today's SCHEDULED habits only.
  const _statusCopy = (key) => {
    const es = (typeof window !== 'undefined' && window.getLang && window.getLang() === 'es');
    const inProgress = es ? 'Sistema de hoy en progreso' : 'Today\u2019s system in progress';
    const complete   = es ? 'Sistema de hoy completo' : 'Today\u2019s system complete';
    const map = {
      low_signal: { label: inProgress, message: es ? 'Completa tu primer hábito de hoy.' : 'Complete your first habit today.' },
      building:   { label: inProgress, message: es ? 'Avanzando hoy.' : 'You\u2019re making progress today.' },
      in_rhythm:  { label: inProgress, message: es ? 'Casi terminas los hábitos de hoy.' : 'You\u2019re close to completing today\u2019s habits.' },
      complete:   { label: complete,   message: es ? 'Completaste los hábitos de hoy.' : 'You completed today\u2019s habits.' },
    };
    return map[key] || map.low_signal;
  };

  // Map a 0–100 completion percent to a status bucket.
  const pickStatus = (pct, totalHabits) => {
    if (totalHabits > 0 && pct >= 100) return 'complete';
    if (pct >= 60) return 'in_rhythm';
    if (pct > 0)   return 'building';
    return 'low_signal';
  };

  const _isES = () => (typeof window !== 'undefined' && window.getLang && window.getLang() === 'es');
  const streakLabel = (n) => {
    const es = _isES();
    if (n >= 30) return es ? 'Cambio de identidad' : 'Identity shift';
    if (n >= 14) return es ? 'Consolidado' : 'Locked in';
    if (n >= 7)  return es ? 'En ritmo' : 'In rhythm';
    if (n >= 3)  return es ? 'Momentum' : 'Momentum';
    if (n >= 1)  return es ? 'Empezaste' : 'Started';
    return es ? 'Empieza hoy' : 'Start today';
  };

  // Streak copy is based on consecutive days where every SCHEDULED habit
  // was completed. Rest / no-scheduled days don\u2019t break the streak.
  const streakMessage = (n, hasHabits = true) => {
    const es = _isES();
    if (!hasHabits)  return es ? 'Añade un hábito para empezar el ritmo.' : 'Add a habit to start the rhythm.';
    if (n >= 30)     return es ? 'Consistencia en los hábitos planeados.' : 'Consistency across planned habits.';
    if (n >= 14)     return es ? 'Ritmo fuerte — sigue así.' : 'Strong rhythm — keep it going.';
    if (n >= 7)      return es ? 'Una semana completa de consistencia.' : 'A full week of consistency.';
    if (n >= 3)      return es ? 'El momentum está creciendo.' : 'Momentum is building.';
    if (n >= 1)      return es ? 'Empezaste el ritmo.' : 'You started the rhythm.';
    return es ? 'Completa los hábitos planeados de hoy para empezar.' : 'Complete today\u2019s scheduled habits to start.';
  };

  // Streak across the *system* (not per-habit): a day counts if every habit
  // that existed by then was checked in. If a precomputed streak is needed
  // per-habit, callers can still use calcStreak(habit.checkIns).
  // TODO: refine once we expose habit.createdAt reliably from all sources.
  // Streak = consecutive days where every SCHEDULED habit was checked in.
  // Days with no scheduled habits do NOT break the streak and do NOT add to it.
  // Returns the count of completed-scheduled-days walking back from today.
  const calcSystemStreak = (habits, todayK) => {
    if (!Array.isArray(habits) || habits.length === 0) return 0;
    const today = new Date();
    let streak = 0;
    let cursor = new Date(today);

    const scheduledFor = (d) => habits.filter(h => isHabitScheduledOn(h, d));
    const dayComplete = (d, k) => {
      const sched = scheduledFor(d);
      if (sched.length === 0) return null; // neutral day
      return sched.every(h => h && h.checkIns && h.checkIns[k]);
    };

    // If today has scheduled habits and they aren't all done, start from yesterday.
    const todayState = dayComplete(cursor, todayK);
    if (todayState === false) cursor.setDate(cursor.getDate() - 1);

    // Walk back. Cap iterations so a long string of neutral days can't loop forever.
    for (let i = 0; i < 365; i++) {
      const k = dKey(cursor);
      const state = dayComplete(cursor, k);
      if (state === true) {
        streak++;
        cursor.setDate(cursor.getDate() - 1);
      } else if (state === null) {
        // Neutral (no scheduled habits) — keep walking, don't break, don't count.
        cursor.setDate(cursor.getDate() - 1);
      } else {
        break;
      }
    }
    return streak;
  };

  // Build the last 7 days (Mon→Sun current week) with per-day habit progress.
  // Labels are language-aware short day-names so chart axes read naturally.
  const buildWeek = (habits) => {
    const es = _isES();
    const labels = es
      ? ['lun','mar','mié','jue','vie','sáb','dom']
      : ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
    const today = new Date();
    const todayK = dKey(today);
    const dow = today.getDay() === 0 ? 7 : today.getDay(); // Mon=1..Sun=7
    const monday = new Date(today); monday.setDate(today.getDate() - (dow - 1));
    const days = [];
    let completeDays = 0;
    const safeHabits = Array.isArray(habits) ? habits : [];
    for (let i = 0; i < 7; i++) {
      const d = new Date(monday); d.setDate(monday.getDate() + i);
      const k = dKey(d);
      const scheduled = safeHabits.filter(h => isHabitScheduledOn(h, d));
      const total = scheduled.length;
      const completed = scheduled.filter(h => h && h.checkIns && h.checkIns[k]).length;
      const isComplete = total > 0 && completed === total;
      const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
      const isFuture = k > todayK;
      if (isComplete) completeDays++;
      days.push({
        date: k, label: labels[i],
        totalHabits: total, completedHabits: completed,
        percent, isFuture,
        isComplete, isToday: k === todayK,
        isRest: total === 0,
      });
    }
    return { days, completeDays };
  };

  // ── Momentum Score ────────────────────────────────────────
  // Simple, documented score for the "how strong is my system today?" signal.
  // Range 0..100. Weights:
  //   60% planned habit completion (continuous)
  //   20% any task completed today (binary)
  //   10% any idea captured today (binary)
  //   10% any reading or focus session today (binary)
  // Kept simple on purpose. NOT a medical/financial metric.
  const computeMomentumScore = ({ habitPct, tasks, ideas, reading, focus }) => {
    const hp = Math.max(0, Math.min(100, Number(habitPct) || 0));
    const habitPart   = (hp / 100) * 60;
    const tasksPart   = (tasks   > 0) ? 20 : 0;
    const ideasPart   = (ideas   > 0) ? 10 : 0;
    const readingPart = ((reading > 0) || (focus > 0)) ? 10 : 0;
    return Math.round(habitPart + tasksPart + ideasPart + readingPart);
  };
  const scoreState = (s) => {
    if (s >= 70) return 'Strong';
    if (s >= 30) return 'Building';
    return 'Low Signal';
  };

  // Map a 0..100 score to a 0..4 intensity bucket for the heatmap.
  const intensityFor = (score) => {
    if (score <= 0)  return 0;
    if (score < 25)  return 1;
    if (score < 50)  return 2;
    if (score < 75)  return 3;
    return 4;
  };

  // Main entry — pure calculation from already-loaded data.
  const calculateMomentum = (input = {}) => {
    const habits         = Array.isArray(input.habits)         ? input.habits         : [];
    const tasks          = Array.isArray(input.tasks)          ? input.tasks          : [];
    const ideas          = Array.isArray(input.ideas)          ? input.ideas          : [];
    const books          = Array.isArray(input.books)          ? input.books          : [];
    const focusSessions  = Array.isArray(input.focusSessions)  ? input.focusSessions  : [];
    const habitCategories = Array.isArray(input.habitCategories) ? input.habitCategories : [];
    // readingEntries is optional — if absent, derive from books[].entries.
    const readingEntries = Array.isArray(input.readingEntries)
      ? input.readingEntries
      : books.flatMap(b => Array.isArray(b && b.entries) ? b.entries : []);

    const todayDate = new Date();
    const today = dKey(todayDate);

    // ── Today: Habits (scheduled only) ────────────────────────
    // "totalHabits" / "completedHabits" reflect ONLY habits scheduled today.
    // Habits not scheduled today (e.g. Gym on a rest day) are excluded so
    // they cannot reduce today's progress or break the streak.
    const scheduledTodayHabits = habits.filter(h => isHabitScheduledOn(h, todayDate));
    const totalHabits     = scheduledTodayHabits.length;
    const completedHabits = scheduledTodayHabits.filter(h => h && h.checkIns && h.checkIns[today]).length;
    const habitsCompletionPercent = totalHabits
      ? Math.round((completedHabits / totalHabits) * 100)
      : 0;
    const isDailyHabitsComplete = totalHabits > 0 && completedHabits === totalHabits;
    const totalHabitsAll = habits.length; // unscheduled-aware "everything in system"

    // ── Today: Tasks ──────────────────────────────────────────
    // A task counts as completed today if either completedAt's local date matches
    // today, or done=true and we don't have completedAt (best-effort fallback).
    const completedTasks = tasks.filter(t => {
      if (!t || !t.done) return false;
      if (!t.completedAt) return true; // legacy rows without completedAt
      const k = String(t.completedAt).slice(0, 10);
      return k === today;
    }).length;
    const activeTasks      = tasks.filter(t => t && !t.done).length;
    const urgentTasks      = tasks.filter(t => t && !t.done && t.pri === 'urgent').length;
    const importantTasks   = tasks.filter(t => t && !t.done && t.pri === 'important').length;
    const laterTasks       = tasks.filter(t => t && !t.done && t.pri === 'later').length;

    // ── Today: Ideas ──────────────────────────────────────────
    const ideasCaptured = ideas.filter(i => i && String(i.createdAt || '').slice(0, 10) === today).length;
    const totalIdeas    = ideas.filter(i => i && !i.archived).length;

    // ── Today: Reading ────────────────────────────────────────
    // A "session" = one entry (note/highlight/progress) logged today.
    const readingSessions = readingEntries.filter(e => e && String(e.date || '').slice(0, 10) === today).length;
    const pagesReadToday  = readingEntries.reduce((acc, e) =>
      acc + ((e && String(e.date || '').slice(0,10) === today) ? (Number(e.pagesRead) || 0) : 0), 0);
    const activeBook = books.find(b => b && b.status === 'reading') || null;
    const currentReadingProgress = activeBook && activeBook.totalPages
      ? Math.min(100, Math.round((activeBook.currentPage || 0) / activeBook.totalPages * 100))
      : 0;

    // ── Today: Focus ──────────────────────────────────────────
    // Focus persistence is not implemented yet — count today only if caller
    // ever passes session rows with a `date` or `startedAt` field.
    const focusToday = focusSessions.filter(s => {
      if (!s) return false;
      const raw = s.date || s.startedAt || s.created_at || '';
      return String(raw).slice(0, 10) === today;
    }).length;

    // ── Historical totals (best-effort, used by Milestones) ──
    // Collect every check-in date across all habits, then derive:
    //   - totalFullHabitDays: days where every CURRENT habit was checked.
    //     Approximation — habit list mutates over time, so historical
    //     "all habits done" treats today's roster as canonical.
    //   - maxHabitsInOneDay: peak completed-habits count on any single day.
    // TODO: revisit once we persist a per-day "active habit set" snapshot.
    const dateCounts = new Map();
    for (const h of habits) {
      const ci = h && h.checkIns;
      if (!ci) continue;
      for (const k of Object.keys(ci)) {
        if (ci[k]) dateCounts.set(k, (dateCounts.get(k) || 0) + 1);
      }
    }
    let totalFullHabitDays = 0;
    let maxHabitsInOneDay = 0;
    for (const [, count] of dateCounts) {
      if (totalHabits > 0 && count >= totalHabits) totalFullHabitDays++;
      if (count > maxHabitsInOneDay) maxHabitsInOneDay = count;
    }

    // TODO (Weekly Consistency): when habits gain a `weeklyTarget` or
    // 'weekly_count' frequency, expose per-habit weekly progress here, e.g.
    //   weeklyConsistency: [{ id, name, target, completedThisWeek }]
    // so MomentumView can render "Gym 2/3 this week", "Reading 4/7", etc.
    // Source-of-truth for weekly windows is buildWeek (Mon→Sun current week).

    const totalCompletedTasks = tasks.filter(t => t && t.done).length;
    const totalReadingSessions = readingEntries.length;
    const totalFocusSessions = focusSessions.length; // 0 until Focus persists

    // ── Weekly ────────────────────────────────────────────────
    const week = buildWeek(habits);

    // ── Weekly consistency, per habit ────────────────────────
    // For the current Mon→Sun week: how many of each habit\u2019s scheduled
    // days were completed. Lets the UI render rows like "Gym 2/4".
    // Per-day status: 'completed' | 'missed' | 'due_today' | 'upcoming'.
    // Days that aren\u2019t scheduled for that habit are skipped (no row).
    // Also derives weekly target progress and catch-up availability so the
    // UI can show "weekly goal can still be recovered".
    const weeklyConsistency = habits.map(h => {
      const days = [];
      let scheduledThisWeek = 0;
      let completedThisWeek = 0;          // completions on scheduled days only
      let missedThisWeek = 0;
      let upcomingThisWeek = 0;
      let dueTodayThisWeek = 0;
      let completedAnyThisWeek = 0;       // any check-in this week (incl. catch-up)
      let remainingDaysInWeek = 0;        // days from today→Sun (incl. today)
      const todayIsScheduled = isHabitScheduledOn(h, new Date());
      for (const d of week.days) {
        const dt = new Date(d.date + 'T00:00:00');
        const checked = !!(h && h.checkIns && h.checkIns[d.date]);
        if (checked) completedAnyThisWeek++;
        if (d.date >= today) remainingDaysInWeek++;
        if (!isHabitScheduledOn(h, dt)) continue;
        scheduledThisWeek++;
        let status;
        if (checked) {
          status = 'completed';
          completedThisWeek++;
        } else if (d.date === today) {
          status = 'due_today';
          dueTodayThisWeek++;
        } else if (d.date < today) {
          status = 'missed';
          missedThisWeek++;
        } else {
          status = 'upcoming';
          upcomingThisWeek++;
        }
        days.push({ date: d.date, label: d.label, status });
      }
      const weeklyTarget = (h.weeklyTarget != null)
        ? h.weeklyTarget
        : (h.frequency === 'custom' ? scheduledThisWeek : 7);
      const targetMet = completedAnyThisWeek >= weeklyTarget;
      // Catch-up is "available today" when:
      //  - the user is behind on their weekly target,
      //  - there\u2019s still time left this week,
      //  - and today is NOT already a scheduled day for this habit (those
      //    just count normally).
      const catchUpAvailableToday =
        !todayIsScheduled &&
        !targetMet &&
        remainingDaysInWeek > 0 &&
        missedThisWeek > 0;
      const recoverable = !targetMet && (dueTodayThisWeek + upcomingThisWeek + (catchUpAvailableToday ? 1 : 0)) >= (weeklyTarget - completedAnyThisWeek);
      return {
        id: h.id, name: h.name, icon: h.icon || 'target',
        frequency: h.frequency || 'daily',
        scheduledThisWeek, completedThisWeek,
        missedThisWeek, upcomingThisWeek, dueTodayThisWeek,
        days,
        // Weekly goal layer
        weeklyTarget,
        completedAnyThisWeek,
        targetMet,
        catchUpAvailableToday,
        todayIsScheduled,
        recoverable,
        remainingDaysInWeek,
      };
    });

    // ── Weekly trend / totals / area balance / reading-by-day ─
    // dailyHabitPercent: per-day Mon→Sun habit completion % (drives line chart).
    // Future days (after today) are emitted with percent=null so the chart can
    // render them as a dashed/empty segment.
    const dailyHabitPercent = week.days.map(d => ({
      date: d.date, label: d.label,
      percent: d.isFuture ? null : d.percent,
      isToday: d.isToday, isRest: d.isRest,
    }));

    // weekTotals: counts of activity within the current Mon→Sun window.
    // Each metric is a per-day breakdown (length 7) plus a sum.
    // Pages read is intentionally NOT computed — we don't track per-session
    // page deltas yet (TODO below). We expose readingSessions instead.
    const weekStartK = week.days[0].date;
    const weekEndK   = week.days[6].date;
    const inWeek = (k) => k >= weekStartK && k <= weekEndK;
    const perDayCount = (predicate) => week.days.map(d => predicate(d.date));

    const habitsCompletedByDay = week.days.map(d => d.completedHabits);
    const tasksCompletedByDay = perDayCount(k => tasks.filter(t => {
      if (!t || !t.done) return false;
      const tk = String(t.completedAt || '').slice(0, 10);
      return tk === k;
    }).length);
    const ideasCapturedByDay = perDayCount(k => ideas.filter(i =>
      i && String(i.createdAt || '').slice(0, 10) === k).length);
    const readingByDay = perDayCount(k => readingEntries.filter(e =>
      e && String(e.date || '').slice(0, 10) === k).length);
    const pagesByDay = week.days.map(d => readingEntries.reduce((acc, e) =>
      acc + ((e && String(e.date || '').slice(0,10) === d.date) ? (Number(e.pagesRead) || 0) : 0), 0));
    const focusByDay = perDayCount(k => focusSessions.filter(s => {
      if (!s) return false;
      const raw = s.date || s.startedAt || s.created_at || '';
      return String(raw).slice(0, 10) === k;
    }).length);

    const sum = (arr) => arr.reduce((a,n) => a + n, 0);
    const weekTotals = {
      habitsCompleted:  sum(habitsCompletedByDay),
      tasksCompleted:   sum(tasksCompletedByDay),
      ideasCaptured:    sum(ideasCapturedByDay),
      readingSessions:  sum(readingByDay),
      focusSessions:    sum(focusByDay),
      // pagesRead: sum of confirmed reading-session page deltas this week.
      pagesRead: sum(pagesByDay),
      byDay: {
        habits:  habitsCompletedByDay,
        tasks:   tasksCompletedByDay,
        ideas:   ideasCapturedByDay,
        reading: readingByDay,
        focus:   focusByDay,
      },
    };

    // areaBalance: per-category weekly habit completion percent. Categories
    // with zero scheduled habits this week are omitted to keep the radar
    // honest. Habits with no categoryId fold into a generic 'uncategorized'
    // bucket only if any exist — otherwise hidden.
    const catById = new Map(habitCategories.map(c => [c.id, c]));
    const areaAgg = new Map(); // id → { name, scheduled, completed }
    for (const row of weeklyConsistency) {
      const h = habits.find(x => x.id === row.id);
      const catId = (h && h.categoryId) || '__uncategorized__';
      const cat = catById.get(catId);
      const name = cat ? cat.name : (catId === '__uncategorized__' ? (_isES() ? 'Sin área' : 'Uncategorized') : 'Other');
      if (!areaAgg.has(catId)) areaAgg.set(catId, { id: catId, name, scheduled: 0, completed: 0, habits: 0 });
      const agg = areaAgg.get(catId);
      agg.scheduled += row.scheduledThisWeek;
      agg.completed += row.completedThisWeek;
      agg.habits    += 1;
    }
    const areaBalance = Array.from(areaAgg.values())
      .filter(a => a.scheduled > 0)
      .map(a => ({
        id: a.id, area: a.name,
        scheduled: a.scheduled, completed: a.completed,
        habits: a.habits,
        percent: a.scheduled ? Math.round((a.completed / a.scheduled) * 100) : 0,
      }))
      .sort((a,b) => b.percent - a.percent);

    // ── Recommendation (rule-based, no AI) ───────────────────
    // Surfaces a gentle "your system may be too heavy" hint when the user
    // has active habits but is consistently not finishing scheduled days.
    // Suppressed when there are no habits or the week is going well.
    let recommendation = null;
    const wkDays = week.days;
    const wkScheduled  = wkDays.reduce((a,d) => a + (d.totalHabits     || 0), 0);
    const wkCompleted  = wkDays.reduce((a,d) => a + (d.completedHabits || 0), 0);
    const wkPct        = wkScheduled ? Math.round((wkCompleted / wkScheduled) * 100) : 0;
    if (totalHabitsAll > 0 && wkScheduled >= 3 && wkPct < 40 && week.completeDays === 0) {
      const _es = _isES();
      recommendation = {
        id: 'reduce_load',
        title: _es ? 'Tu sistema puede ser demasiado pesado.' : 'Your system may be too heavy.',
        body:  _es ? 'Reduce un hábito a menos días esta semana. Consistencia primero, intensidad después.' : 'Try reducing one habit to fewer days this week. Consistency first, intensity later.',
        cta:   { label: _es ? 'Ajustar hábitos' : 'Adjust habits', route: 'habits' },
      };
    }

    // ── Daily metrics + Momentum Score (weekly) ──────────────
    // Per-day rich metric record used by the interactive trend chart.
    // Future days have momentumScore = null so the chart can render an
    // empty/dashed segment safely.
    const weeklyDaily = week.days.map((d, i) => {
      const habitPct = d.totalHabits > 0 ? d.percent : 0;
      const t = tasksCompletedByDay[i] || 0;
      const id = ideasCapturedByDay[i] || 0;
      const r = readingByDay[i] || 0;
      const f = focusByDay[i] || 0;
      const score = d.isFuture ? null : computeMomentumScore({
        habitPct, tasks: t, ideas: id, reading: r, focus: f
      });
      return {
        date: d.date,
        dayKey: d.date,
        label: d.label,
        isToday: d.isToday,
        isFuture: d.isFuture,
        isRest: d.isRest,
        momentumScore: score,
        habitCompletionPercent: d.isFuture ? null : habitPct,
        tasksCompleted: t,
        ideasCaptured: id,
        readingSessions: r,
        pagesRead: 0, // TODO: track confirmed page deltas from Done for Today.
        focusSessions: f,
      };
    });
    const todayScore = computeMomentumScore({
      habitPct: habitsCompletionPercent,
      tasks: completedTasks, ideas: ideasCaptured,
      reading: readingSessions, focus: focusToday,
    });

    // ── Heatmap (last 30 days) ───────────────────────────────
    // Compute daily score for each of the last 30 calendar days. Cheap O(n*habits)
    // scan; safe with empty inputs.
    const heatmap = [];
    for (let back = 29; back >= 0; back--) {
      const d = new Date(todayDate); d.setDate(todayDate.getDate() - back);
      const k = dKey(d);
      const sched = habits.filter(h => isHabitScheduledOn(h, d));
      const compl = sched.filter(h => h && h.checkIns && h.checkIns[k]).length;
      const hpct  = sched.length ? Math.round((compl / sched.length) * 100) : 0;
      const tC = tasks.filter(t => t && t.done && String(t.completedAt || '').slice(0,10) === k).length;
      const iC = ideas.filter(i => i && String(i.createdAt || '').slice(0,10) === k).length;
      const rC = readingEntries.filter(e => e && String(e.date || '').slice(0,10) === k).length;
      const fC = focusSessions.filter(s => {
        const raw = (s && (s.date || s.startedAt || s.created_at)) || '';
        return String(raw).slice(0,10) === k;
      }).length;
      const score = computeMomentumScore({ habitPct: hpct, tasks: tC, ideas: iC, reading: rC, focus: fC });
      heatmap.push({
        date: k,
        momentumScore: score,
        intensity: intensityFor(score),
        habitsCompleted: compl,
        habitsScheduled: sched.length,
        tasksCompleted: tC,
        ideasCaptured: iC,
        readingSessions: rC,
        focusSessions: fC,
        isToday: k === today,
      });
    }

    // ── Monthly per-habit consistency (month-to-date) ────────
    // For each habit, count scheduled days from the LATER of (1st of current
    // month) and (habit.createdAt) through today, inclusive. Future days are
    // skipped. Rest days (non-scheduled weekdays for custom habits) don't
    // count against the habit. Extras (check-ins on non-scheduled days) do
    // not inflate the target.
    const monthStart = new Date(todayDate.getFullYear(), todayDate.getMonth(), 1);
    const stripTime = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
    const todayMidnight = stripTime(todayDate);

    // Frequency-only schedule check (no createdAt gate). Used so the monthly
    // helper can apply its own createdAt range without double-blocking.
    const _isScheduledByFrequency = (habit, date) => {
      if (!habit) return false;
      const f = habit.frequency || 'daily';
      if (f === 'daily') return true;
      if (f === 'custom') {
        const days = Array.isArray(habit.customDays) ? habit.customDays : [];
        return days.includes(date.getDay());
      }
      return false;
    };

    const countScheduledHabitInstances = (habit, startDate, endDate) => {
      const out = { scheduled: 0, completed: 0 };
      if (!habit) return out;
      const start = stripTime(startDate);
      const end   = stripTime(endDate);
      if (start > end) return out;
      const cur = new Date(start);
      while (cur <= end) {
        if (_isScheduledByFrequency(habit, cur)) {
          out.scheduled++;
          const k = dKey(cur);
          if (habit.checkIns && habit.checkIns[k]) out.completed++;
        }
        cur.setDate(cur.getDate() + 1);
      }
      return out;
    };

    const monthlyConsistency = habits.map(h => {
      // Start = max(monthStart, habit.createdAt). Falls back to monthStart
      // if createdAt is missing or unparseable. createdAt may be a UTC ISO
      // string sliced via `.slice(0,10)`; we re-parse as a local midnight.
      let rangeStart = monthStart;
      if (h && h.createdAt) {
        const c = String(h.createdAt).slice(0, 10);
        if (/^\d{4}-\d{2}-\d{2}$/.test(c)) {
          const [yy, mm, dd] = c.split('-').map(Number);
          const cd = new Date(yy, mm - 1, dd);
          if (cd > rangeStart) rangeStart = cd;
        }
      }
      const { scheduled, completed } =
        countScheduledHabitInstances(h, rangeStart, todayMidnight);
      const percent = scheduled ? Math.round((completed / scheduled) * 100) : 0;
      return { id: h.id, name: h.name,
        schedule_type: h.frequency || 'daily',
        schedule_days: h.customDays || null,
        scheduled, completed, percent };
    });

    // Temporary diagnostic — opt-in only. Set window.VYB_DEBUG_MOMENTUM = true.
    if (typeof window !== 'undefined' && window.VYB_DEBUG_MOMENTUM === true) {
      try {
        // eslint-disable-next-line no-console
        console.log('[Monthly habit insight debug]', {
          monthStart: dKey(monthStart),
          today: today,
          habits: monthlyConsistency.map(h => ({
            name: h.name,
            schedule_type: h.schedule_type,
            schedule_days: h.schedule_days,
            completed: h.completed,
            target: h.scheduled,
            percent: h.percent,
          })),
        });
      } catch {}
    }

    // ── Monthly section data (month-to-date) ─────────────────
    // Build a per-day record for every day from the 1st of the current
    // month → today, with habit progress, activity counts, score, and
    // intensity bucket used by the Monthly Momentum Map heatmap.
    const monthEnd = todayMidnight;
    const monthDays = [];
    let monthHabitsCompleted = 0;
    let monthHabitsTarget    = 0;
    let monthExtrasCompleted = 0;
    let monthTasksCompleted  = 0;
    let monthIdeasCaptured   = 0;
    let monthReadingSessions = 0;
    let monthPagesRead       = 0;
    let monthFocusSessions   = 0;
    let completeDaysCount    = 0;
    let activeDaysCount      = 0;
    const monthAreaAgg = new Map(); // categoryId → { id, name, scheduled, completed, habits }
    {
      const cur = new Date(monthStart);
      while (cur <= monthEnd) {
        const k = dKey(cur);
        const sched = habits.filter(h => isHabitScheduledOn(h, cur));
        const compl = sched.filter(h => h && h.checkIns && h.checkIns[k]).length;
        const plannedTotal = sched.length;
        const habitPct = plannedTotal ? Math.round((compl / plannedTotal) * 100) : 0;

        const tC = tasks.filter(t => t && t.done && String(t.completedAt || '').slice(0,10) === k).length;
        const iC = ideas.filter(i => i && String(i.createdAt || '').slice(0,10) === k).length;
        const rC = readingEntries.filter(e => e && String(e.date || '').slice(0,10) === k).length;
        const pC = readingEntries.reduce((acc, e) =>
          acc + ((e && String(e.date || '').slice(0,10) === k) ? (Number(e.pagesRead) || 0) : 0), 0);
        const fC = focusSessions.filter(s => {
          const raw = (s && (s.date || s.startedAt || s.created_at)) || '';
          return String(raw).slice(0,10) === k;
        }).length;
        const extras = (habits || []).filter(h =>
          h && h.checkIns && h.checkIns[k] && !isHabitScheduledOn(h, cur)
        ).length;

        const score = computeMomentumScore({ habitPct, tasks: tC, ideas: iC, reading: rC, focus: fC });

        const hasActivity = tC > 0 || iC > 0 || rC > 0 || fC > 0 || extras > 0 || compl > 0;
        let intensity;
        if (!hasActivity) {
          intensity = plannedTotal === 0 ? 'rest' : 'none';
        } else if (plannedTotal === 0) {
          intensity = 'low';
        } else if (habitPct >= 100) {
          intensity = (extras > 0 || tC > 0 || iC > 0 || rC > 0 || fC > 0) ? 'glow' : 'complete';
        } else if (habitPct >= 70) {
          intensity = 'strong';
        } else if (habitPct > 0) {
          intensity = 'medium';
        } else {
          intensity = 'low';
        }

        const isTodayCell = k === today;
        const isPlannedComplete = plannedTotal > 0 && compl === plannedTotal;
        if (isPlannedComplete) completeDaysCount++;
        if (hasActivity) activeDaysCount++;

        monthDays.push({
          date: k,
          dayNumber: cur.getDate(),
          weekday: cur.getDay(),
          plannedHabitsTotal: plannedTotal,
          plannedHabitsCompleted: compl,
          habitPercent: habitPct,
          tasksCompleted: tC,
          ideasCaptured: iC,
          readingSessions: rC,
          pagesRead: pC,
          focusSessions: fC,
          extrasCompleted: extras,
          score,
          intensity,
          isToday: isTodayCell,
        });

        monthHabitsCompleted += compl;
        monthHabitsTarget    += plannedTotal;
        monthExtrasCompleted += extras;
        monthTasksCompleted  += tC;
        monthIdeasCaptured   += iC;
        monthReadingSessions += rC;
        monthPagesRead       += pC;
        monthFocusSessions   += fC;
        cur.setDate(cur.getDate() + 1);
      }
    }

    // Monthly area balance (parallels weekly areaBalance).
    for (const row of monthlyConsistency) {
      const h = habits.find(x => x.id === row.id);
      const catId = (h && h.categoryId) || '__uncategorized__';
      const cat = catById.get(catId);
      const name = cat ? cat.name : (catId === '__uncategorized__' ? (_isES() ? 'Sin área' : 'Uncategorized') : 'Other');
      if (!monthAreaAgg.has(catId)) monthAreaAgg.set(catId, { id: catId, name, scheduled: 0, completed: 0, habits: 0 });
      const agg = monthAreaAgg.get(catId);
      agg.scheduled += row.scheduled;
      agg.completed += row.completed;
      agg.habits    += 1;
    }
    const monthAreaBalance = Array.from(monthAreaAgg.values())
      .filter(a => a.scheduled > 0)
      .map(a => ({
        id: a.id, area: a.name,
        scheduled: a.scheduled, completed: a.completed,
        habits: a.habits,
        percent: a.scheduled ? Math.round((a.completed / a.scheduled) * 100) : 0,
      }))
      .sort((a,b) => b.percent - a.percent);

    // Best / needs-support habit for the month.
    const _scoreMonthly = (r) => {
      const target    = r.scheduled || 0;
      const completed = r.completed || 0;
      const pct       = target ? completed / target : 0;
      const missed    = Math.max(0, target - completed);
      return {
        bestScore:    pct * 0.6 + Math.min(completed/5,1)*0.25 + Math.min(target/5,1)*0.15,
        supportScore: (1-pct)*0.6 + Math.min(missed/5,1)*0.3 + Math.min(target/5,1)*0.1,
      };
    };
    const monthHabitRank = monthlyConsistency.map((r, idx) => ({ ...r, _idx: idx, ..._scoreMonthly(r) }));
    const bestMonthHabit = monthHabitRank.length
      ? [...monthHabitRank].sort((a,b) =>
          b.bestScore - a.bestScore || b.scheduled - a.scheduled || b.completed - a.completed || a._idx - b._idx)[0]
      : null;
    const supportMonthHabit = monthHabitRank.length
      ? [...monthHabitRank].sort((a,b) =>
          b.supportScore - a.supportScore || b.scheduled - a.scheduled || b.completed - a.completed || a._idx - b._idx)[0]
      : null;

    const strongestMonthArea    = monthAreaBalance[0] || null;
    const needsSupportMonthArea = monthAreaBalance.length
      ? monthAreaBalance[monthAreaBalance.length - 1] : null;

    // Monthly reading roll-up. Books finished = books moved to 'completed'
    // status whose updatedAt (or any entry) falls in this month — best effort.
    const booksFinishedThisMonth = books.filter(b => {
      if (!b || b.status !== 'completed') return false;
      const stamps = [];
      if (b.updatedAt) stamps.push(String(b.updatedAt).slice(0,10));
      if (b.completedAt) stamps.push(String(b.completedAt).slice(0,10));
      if (Array.isArray(b.entries)) {
        for (const e of b.entries) {
          if (e && e.date) stamps.push(String(e.date).slice(0,10));
        }
      }
      return stamps.some(s => s >= dKey(monthStart) && s <= today);
    }).length;
    const monthlyReading = {
      sessions:       monthReadingSessions,
      pagesRead:      monthPagesRead,
      booksFinished:  booksFinishedThisMonth,
      hasReliablePages: monthPagesRead > 0 || monthReadingSessions > 0,
    };

    const _monthName = (() => {
      try {
        return todayDate.toLocaleString(_isES() ? 'es' : 'en', { month: 'long', year: 'numeric' });
      } catch { return `${todayDate.getFullYear()}-${pad2(todayDate.getMonth()+1)}`; }
    })();
    const monthPeriod = {
      label: _monthName,
      startDate: dKey(monthStart),
      endDate: today,
      daysElapsed: monthDays.length,
      daysInMonth: new Date(todayDate.getFullYear(), todayDate.getMonth() + 1, 0).getDate(),
    };

    const monthTotals = {
      habitsCompleted: monthHabitsCompleted,
      habitsTarget:    monthHabitsTarget,
      extrasCompleted: monthExtrasCompleted,
      tasksCompleted:  monthTasksCompleted,
      ideasCaptured:   monthIdeasCaptured,
      readingSessions: monthReadingSessions,
      pagesRead:       monthPagesRead,
      focusSessions:   monthFocusSessions,
    };

    // ── Today Signals ────────────────────────────────────────
    // Derived from CURRENT state (not an event log). If the user un-checks a
    // habit, the corresponding signal disappears next render. Generic labels
    // only — no habit/task/idea names — so this surface is safe for future
    // social use.
    const signalsToday = [];
    if (totalHabits > 0 && isDailyHabitsComplete) {
      signalsToday.push({ type: 'habits_system_complete', label: 'Today\u2019s system complete' });
    } else if (completedHabits > 0) {
      signalsToday.push({
        type: 'habits_partial',
        label: `${completedHabits} of ${totalHabits} planned habit${totalHabits===1?'':'s'} complete`,
      });
    }
    if (completedTasks > 0) {
      signalsToday.push({
        type: 'tasks_completed',
        label: `${completedTasks} task${completedTasks===1?'':'s'} completed`,
      });
    }
    if (ideasCaptured > 0) {
      signalsToday.push({
        type: 'ideas_captured',
        label: `${ideasCaptured} idea${ideasCaptured===1?'':'s'} captured`,
      });
    }
    if (readingSessions > 0) {
      signalsToday.push({
        type: 'reading_signal',
        label: `${readingSessions} reading session${readingSessions===1?'':'s'}`,
      });
    }
    if (focusToday > 0) {
      signalsToday.push({
        type: 'focus_signal',
        label: `${focusToday} focus session${focusToday===1?'':'s'}`,
      });
    }

    // ── Streak (system-wide habits) ───────────────────────────
    const currentStreak = calcSystemStreak(habits, today);

    // ── Status ────────────────────────────────────────────────
    const statusKey = pickStatus(habitsCompletionPercent, totalHabits);
    const status = { key: statusKey, ..._statusCopy(statusKey) };

    return {
      today: {
        totalHabits,                 // scheduled today
        completedHabits,             // scheduled today AND checked
        habitsCompletionPercent,     // 0..100 over scheduled today
        isDailyHabitsComplete,       // true when scheduled>0 and all checked
        totalHabitsAll,              // every habit in system (unfiltered)
        // Aliases — clearer names for the new "Today's System" surface.
        scheduledHabitsToday: totalHabits,
        completedScheduledHabitsToday: completedHabits,
        scheduledHabitsCompletionPercent: habitsCompletionPercent,
        isTodaySystemComplete: isDailyHabitsComplete,
        completedTasks,
        activeTasks,
        urgentTasks,
        importantTasks,
        laterTasks,
        ideasCaptured,
        totalIdeas,
        readingSessions,
        pagesRead: pagesReadToday,
        currentReadingProgress,
        focusSessions: focusToday,
        // v2: simple Momentum Score for today (0..100) + label.
        momentumScore: todayScore,
        momentumState: scoreState(todayScore),
      },
      weekly: {
        completeHabitDays: week.completeDays,
        totalDays: 7,
        days: week.days,
        consistency: weeklyConsistency,
        scheduledThisWeek: wkScheduled,
        completedThisWeek: wkCompleted,
        completionPercent: wkPct,
        // New for the analytics dashboard (Phase 1 charts):
        dailyHabitPercent,   // [{date,label,percent|null,isToday,isRest}]
        totals: weekTotals,  // { habitsCompleted, tasksCompleted, ... , byDay:{...} }
        areaBalance,         // [{id,area,scheduled,completed,habits,percent}]
        readingByDay,        // [n0..n6] reading sessions per day Mon→Sun
        // v2: rich per-day metric records for the interactive trend chart.
        daily: weeklyDaily,
      },
      // v2: 30-day momentum heatmap + month-to-date per-habit consistency.
      month: {
        heatmap,
        consistency: monthlyConsistency,
        period: monthPeriod,
        days: monthDays,
        totals: monthTotals,
        completeDays: completeDaysCount,
        activeDays:   activeDaysCount,
        bestHabit:         bestMonthHabit ? {
          id: bestMonthHabit.id, name: bestMonthHabit.name,
          completed: bestMonthHabit.completed, target: bestMonthHabit.scheduled,
          percent: bestMonthHabit.percent } : null,
        needsSupportHabit: supportMonthHabit ? {
          id: supportMonthHabit.id, name: supportMonthHabit.name,
          completed: supportMonthHabit.completed, target: supportMonthHabit.scheduled,
          percent: supportMonthHabit.percent } : null,
        areaBalance:       monthAreaBalance,
        strongestArea:     strongestMonthArea,
        needsSupportArea:  needsSupportMonthArea,
        reading:           monthlyReading,
      },
      // v2: derived-from-current-state today signals (no event log).
      signalsToday,
      recommendation,
      streak: {
        current: currentStreak,
        label: streakLabel(currentStreak),
        message: streakMessage(currentStreak, totalHabits > 0),
      },
      totals: {
        fullHabitDays: totalFullHabitDays,
        maxHabitsInOneDay,
        ideas: ideas.filter(i => i && !i.archived).length,
        completedTasks: totalCompletedTasks,
        readingSessions: totalReadingSessions,
        focusSessions: totalFocusSessions,
      },
      status,
    };
  };

  // Expose globally (matches the no-build, window-scoped pattern used by
  // the rest of the dashboard files).
  // ── Progress Copy System ──────────────────────────────────
  // Rule-based, deterministic, no AI. Translates a momentum object into
  // short premium microcopy used across Overview, Hero, Today's Signal,
  // Rhythm, Seal, and Milestones. All values fall back gracefully so a
  // missing momentum object never crashes a consumer.
  const getMomentumCopy = (m) => {
    const today   = (m && m.today)   || {};
    const weekly  = (m && m.weekly)  || {};
    const streak  = (m && m.streak)  || {};
    const totals  = (m && m.totals)  || {};
    const totalHabits = today.totalHabits || 0;
    const pct = today.habitsCompletionPercent || 0;
    const isComplete = !!today.isDailyHabitsComplete;

    // Tone bucket — drives accent decisions in consumers.
    const tone =
      isComplete                       ? 'complete'
      : (totalHabits === 0 || pct === 0) ? 'low'
      : pct < 40                       ? 'low'
      : pct < 80                       ? 'building'
      :                                  'rhythm';

    const es = _isES();
    // Hero / today line by habit progress band.
    let heroLine, todayMessage;
    if (totalHabits === 0) {
      heroLine     = es ? 'Sin hábitos planeados para hoy.' : 'No habits planned for today.';
      todayMessage = es ? 'Añade un hábito para hoy en Hábitos.' : 'Add a habit for today in Habits.';
    } else if (isComplete) {
      heroLine     = es ? 'Sistema de hoy completo.' : 'Today\u2019s system complete.';
      todayMessage = es ? 'Completaste los hábitos de hoy.' : 'You completed today\u2019s habits.';
    } else if (pct === 0) {
      heroLine     = es ? 'El día está abierto.' : 'The day is open.';
      todayMessage = es ? 'Completa los hábitos de hoy.' : 'Complete today\u2019s habits.';
    } else if (pct < 40) {
      heroLine     = es ? 'Avanzando hoy.' : 'You\u2019re making progress today.';
      todayMessage = es ? 'Sigue — un hábito a la vez.' : 'Keep going — one habit at a time.';
    } else if (pct < 80) {
      heroLine     = es ? 'Avanzando hoy.' : 'You\u2019re making progress today.';
      todayMessage = es ? 'Te quedan unos hábitos para hoy.' : 'A few habits left for today.';
    } else {
      heroLine     = es ? 'Casi terminas los hábitos de hoy.' : 'You\u2019re close to completing today\u2019s habits.';
      todayMessage = es ? 'Casi terminas el sistema de hoy.' : 'Almost done with today\u2019s system.';
    }

    // Next action — concrete, cascades habits → tasks → ideas → reading → focus → review.
    let nextAction;
    if (totalHabits === 0) {
      nextAction = es ? 'Añade un hábito para hoy.' : 'Add a habit for today.';
    } else if (!isComplete) {
      nextAction = es ? 'Completa tu próximo hábito.' : 'Complete your next habit.';
    } else if ((today.completedTasks || 0) === 0 && (today.activeTasks || 0) > 0) {
      nextAction = es ? 'Termina una tarea.' : 'Finish one task.';
    } else if ((today.ideasCaptured || 0) === 0) {
      nextAction = es ? 'Captura una idea.' : 'Capture one idea.';
    } else if ((today.readingSessions || 0) === 0) {
      nextAction = es ? 'Lee unos minutos.' : 'Read for a few minutes.';
    } else if ((today.focusSessions || 0) === 0) {
      nextAction = es ? 'Inicia una sesión de enfoque.' : 'Start a focus session.';
    } else {
      nextAction = es ? 'Revisa tu progreso.' : 'Review your progress.';
    }

    // Rhythm copy = streak microcopy (already handled by streakMessage).
    const rhythmMessage = streakMessage(streak.current || 0, totalHabits > 0);

    // Weekly copy by complete-day count.
    const wd = weekly.completeHabitDays || 0;
    // Weekly copy reflects consistency across scheduled habits, not perfection.
    const weeklyMessage =
      wd >= 7 ? (es ? 'Semana completa de consistencia.' : 'Full week of consistency.')
      : wd >= 5 ? (es ? 'Semana fuerte — mantén el ritmo.' : 'Strong week — keep the rhythm.')
      : wd >= 3 ? (es ? 'La consistencia empieza a verse.' : 'Consistency is starting to show.')
      : wd >= 1 ? (es ? 'Aparecen las primeras señales.' : 'Early signals are forming.')
      :           (es ? 'La semana sigue abierta.' : 'The week is still open.');

    // Seal copy is meaningful only when today is complete.
    const sealMessage = isComplete
      ? (es ? 'Vuelve mañana y repite la señal.' : 'Come back tomorrow and repeat the signal.')
      : (totalHabits === 0
          ? (es ? 'Sin hábitos planeados para hoy.' : 'No habits planned for today.')
          : (es ? 'Termina los hábitos de hoy para asegurar el momentum.' : 'Finish today\u2019s habits to lock momentum.'));

    // Milestones copy — derived from unlocked count via totals if present,
    // otherwise from a heuristic based on streak/full days.
    const unlockedHint =
      (totals.fullHabitDays || 0) +
      ((totals.ideas || 0) >= 10 ? 1 : 0) +
      ((totals.completedTasks || 0) >= 10 ? 1 : 0) +
      ((totals.readingSessions || 0) >= 3 ? 1 : 0);
    const milestonesMessage =
      unlockedHint >= 4 ? (es ? 'Tu sistema está dejando huella.' : 'Your system is leaving evidence.')
      : unlockedHint >= 1 ? (es ? 'La prueba de progreso se hace visible.' : 'Proof of progress is becoming visible.')
      :                     (es ? 'Empieza pequeño. La primera señal importa.' : 'Start small. The first signal matters.');

    return {
      heroLine,
      todayMessage,
      rhythmMessage,
      weeklyMessage,
      sealMessage,
      milestonesMessage,
      nextAction,
      tone,
    };
  };

  // ── Future-ready scaffolding (Step 10) ────────────────────
  // None of this executes AI, fetches anything, or persists data. It is
  // a documented contract so a future premium / AI layer can plug in
  // without redesigning the Momentum surface.
  //
  // Planned insight categories (rule-based or AI-backed later):
  //   weekly_reflection      — "5 full habit days this week. Strongest area: Health."
  //   habit_pattern_insight  — "Momentum dips after Wednesday."
  //   focus_recommendation   — "Tasks completion correlates with focus sessions."
  //   idea_pattern_insight   — "12 ideas captured this week. 5 were business."
  //   reading_connection     — "Reading notes overlap with your business ideas."
  //   next_week_plan         — "Next signal: protect 3 deep work blocks."
  //   identity_coach         — long-arc reflection on streak / identity tier.
  //
  // Future premium surfaces (NOT implemented yet):
  //   - AI Weekly Reflection
  //   - AI Habit Builder
  //   - AI Identity Coach
  //   - AI Voice Capture Parser
  //   - Smart Weekly Plan
  //   - Pattern Detection
  //   - Personalized Momentum Recommendations
  const MOMENTUM_INSIGHT_TYPES = [
    { id: 'weekly_reflection',     label: 'Weekly Reflection',     premium: true },
    { id: 'habit_pattern_insight', label: 'Habit Pattern Insight', premium: true },
    { id: 'focus_recommendation',  label: 'Focus Recommendation',  premium: true },
    { id: 'idea_pattern_insight',  label: 'Idea Pattern Insight',  premium: true },
    { id: 'reading_connection',    label: 'Reading Connection',    premium: true },
    { id: 'next_week_plan',        label: 'Next Week Plan',        premium: true },
    { id: 'identity_coach',        label: 'Identity Coach',        premium: true },
  ];

  // Pure normalizer: shapes the local momentum + a few raw signals into the
  // snapshot a future AI layer could consume. No network, no mutation.
  // Callers MUST treat the output as read-only.
  const buildMomentumSnapshot = (momentum, raw = {}) => {
    const m = momentum || {};
    return {
      date: dKey(new Date()),
      today: m.today || null,
      weekly: m.weekly || null,
      streak: m.streak || null,
      totals: m.totals || null,
      status: m.status || null,
      // Raw counts only — no PII, no titles unless caller passes them in.
      breakdown: {
        habits:   { count: (raw.habits || []).length },
        tasks:    { count: (raw.tasks || []).length },
        ideas:    { count: (raw.ideas || []).length },
        books:    { count: (raw.books || []).length },
        // TODO: focusSessions once Focus persistence ships.
        focus:    { count: (raw.focusSessions || []).length },
      },
      // TODO: per-area trends, per-day timeseries — fill once we persist
      // daily snapshots rather than relying on live state.
    };
  };

  window.isHabitScheduledOn = isHabitScheduledOn;
  window.calculateMomentum = calculateMomentum;
  window.momentumStreakLabel = streakLabel;
  window.momentumStreakMessage = streakMessage;
  window.getMomentumCopy = getMomentumCopy;
  window.MOMENTUM_INSIGHT_TYPES = MOMENTUM_INSIGHT_TYPES;
  window.buildMomentumSnapshot = buildMomentumSnapshot;
})();
