// Vanta — live data layer. Fetches real state from the server and overlays it
// onto the design's seed data (VANTA_DATA) before the app mounts. If the
// server has no database (or the fetch fails/times out), everything falls
// back to the design exactly as shipped. Visuals are never altered here —
// only values.

window.VANTA_LIVE = null;

// Fire-and-forget write-back; the UI never waits on these. Writes that change
// habit/objective/chore/mood state invalidate the cached rhythm feed and fire
// a 'vanta:changed' event once the write lands, so the rhythm calendar and
// streak refresh instead of showing this session's stale snapshot.
function vantaPost(path, body) {
  const affectsRhythm = /\/vanta\/(habit|mood|move|chore|task)/.test(path);
  try {
    const p = fetch(path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (affectsRhythm) {
      p.then(() => {
        window.VANTA_RHYTHM = null;
        try { window.dispatchEvent(new Event('vanta:changed')); } catch {}
      }).catch(() => {});
    } else {
      p.catch(() => {});
    }
  } catch {}
}

// Quiet literary numbers for the habits headline ("eighteen-day streak").
function vantaNumWord(n) {
  const ones = ['zero','one','two','three','four','five','six','seven','eight','nine','ten',
    'eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen','nineteen'];
  const tens = ['','','twenty','thirty','forty','fifty','sixty','seventy','eighty','ninety'];
  if (n < 20) return ones[n];
  if (n < 100) return tens[Math.floor(n / 10)] + (n % 10 ? '-' + ones[n % 10] : '');
  return String(n);
}

function vantaDueLabel(iso) {
  // Date-only values (and midnight-UTC timestamps from Postgres DATE columns)
  // must not shift a day in negative-offset timezones — pin to local noon.
  const s = String(iso);
  const m = s.match(/^(\d{4}-\d{2}-\d{2})/);
  const d = m ? new Date(m[1] + 'T12:00:00') : new Date(s);
  if (isNaN(d)) return '';
  const days = ['sun','mon','tue','wed','thu','fri','sat'];
  const months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'];
  return `${days[d.getDay()]} · ${months[d.getMonth()]} ${d.getDate()}`;
}

// Time-of-day salutation + real date — applied at load, before the app mounts.
(function () {
  const D = window.VANTA_DATA;
  D.user.date = new Date();
  const h = new Date().getHours();
  const part = h < 5 ? 'evening' : h < 12 ? 'morning' : h < 18 ? 'afternoon' : 'evening';
  D.greeting.salutation = `Good ${part}, ${D.user.name}.`;
})();

// Merge server thoughts into the persisted store so the app boots with them.
function vantaMergeThoughts(serverThoughts) {
  if (!serverThoughts || !serverThoughts.length) return;
  try {
    const KEY = 'vanta.app.v1';
    const saved = JSON.parse(localStorage.getItem(KEY) || '{}');
    const local = Array.isArray(saved.thoughts) ? saved.thoughts : [];
    const isDupe = (a, b) => a.text === b.text && Math.abs(a.ts - b.ts) < 60000;
    const merged = [...local];
    for (const t of serverThoughts) {
      if (!merged.some(m => isDupe(m, t))) merged.push(t);
    }
    merged.sort((a, b) => b.ts - a.ts);
    saved.thoughts = merged.slice(0, 50);
    localStorage.setItem(KEY, JSON.stringify(saved));
  } catch {}
}

// One tap on a loggable detail row — chores mark done, hobbies and habits
// log today, work objectives complete. Mutates the row + its area's summary
// and fires the write-back; returns true when something changed so the
// caller can re-render. Shared by the mobile card body and desktop tiles.
function vantaTapAct(d, data) {
  if (!d || !d.act) return false;
  const D = data || window.VANTA_DATA;
  if (d.act.type === 'chore') {
    if (d.pct === 1) return false;
    vantaPost('/vanta/chore/done', { id: d.act.id });
    d.v = 'done'; d.pct = 1;
    const home = D.areas.home;
    const due = home.details.filter(r => r.pct !== 1);
    home.headline = due.length ? `${due.length} to do` : 'all tended';
    home.sub = due.length ? due.slice(0, 3).map(r => r.k.toLowerCase()).join(' · ') : 'nothing due today';
    return true;
  }
  if (d.act.type === 'hobby') {
    if (d.act.doneToday) return false;
    vantaPost('/vanta/habit', { habit: d.act.key, completed: true });
    d.act.doneToday = true;
    d.act.count += 1;
    d.v = `${d.act.count} / wk`;
    if (d.act.goal) d.pct = Math.min(1, d.act.count / d.act.goal);
    return true;
  }
  if (d.act.type === 'habit') {
    // Habits toggle both ways — an accidental check can be undone anywhere.
    const next = !d.done;
    vantaPost('/vanta/habit', { habit: d.act.key, completed: next });
    d.done = next; d.v = next ? '✓' : '·';
    const hab = D.areas.habits;
    const done = hab.details.filter(r => r.done).length;
    hab.headline = `${done} of ${hab.details.length}`;
    return true;
  }
  if (d.act.type === 'move') {
    if (d.pct === 1) return false;
    vantaPost('/vanta/move', { id: d.act.id });
    d.v = '✓'; d.pct = 1;
    const work = D.areas.work;
    const done = work.details.filter(r => r.pct === 1).length;
    work.headline = `${done} / ${work.details.length} done`;
    const lm = window.VANTA_LIVE?.moves?.find(m => m.id === d.act.id);
    if (lm) lm.done = true;
    return true;
  }
  return false;
}

// Fetch live data and overlay it onto VANTA_DATA. Resolves regardless of
// outcome — the caller renders either way.
function vantaLoadLive() {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), 2000);
  return fetch('/vanta/data', { signal: ctrl.signal })
    .then(r => (r.ok ? r.json() : null))
    .then(live => {
      if (!live) return;
      const D = window.VANTA_DATA;
      const L = { streak: live.streak };
      L.glance = live.glance || null;   // Level 0: the day's one honest line

      // Habits — real list, today's checks, per-habit streaks.
      if (live.habits && live.habits.length) {
        D.areas.habits.details = live.habits.map(h => ({
          k: h.label, hk: h.key, v: h.done ? '✓' : '·', streak: h.streak, done: h.done,
          act: { type: 'habit', key: h.key },
        }));
        const done = live.habits.filter(h => h.done).length;
        D.areas.habits.headline = `${done} of ${live.habits.length}`;
        if (typeof live.streak === 'number') {
          D.areas.habits.sub = live.streak > 0 ? `${live.streak}-day streak` : 'a fresh page';
          L.streakText = live.streak > 0 ? `${vantaNumWord(live.streak)}-day streak` : 'no streak yet';
        }
      }

      // Moves — today's tasks with steps + carried flag. An empty array still
      // means "DB reachable, zero tasks" (server mode); only null (DB down)
      // leaves L.moves undefined so the shell falls back to demo tasks.
      if (Array.isArray(live.moves)) {
        L.moves = live.moves.map(m => ({
          id: m.id, t: m.text, text: m.text, done: m.done,
          carried: !!m.carried, steps: Array.isArray(m.steps) ? m.steps : [], area: null,
        }));
      }

      // Learning — the current book leads, course deadlines follow. Both are
      // real; neither replaces the other. Deadline titles link straight to
      // Brightspace when we have it (course homepage as fallback), a web
      // search only when we have neither.
      {
        const deadlineRows = (live.deadlines || []).map(d => ({
          k: d.title || 'untitled', v: vantaDueLabel(d.due_date), goal: d.course || '', pct: null,
          ...(d.url
            ? { href: d.url }
            : d.title
              ? { href: `https://www.google.com/search?q=${encodeURIComponent([d.course, d.title, 'brightspace'].filter(Boolean).join(' '))}` }
              : {}),
        }));
        const bk = live.book;
        const bookRows = bk ? [{
          k: bk.title,
          v: bk.totalPages ? `p. ${bk.currentPage} / ${bk.totalPages}` : `p. ${bk.currentPage}`,
          goal: bk.author || '', pct: bk.pct,
          href: bk.url || `https://www.goodreads.com/search?q=${encodeURIComponent([bk.title, bk.author].filter(Boolean).join(' '))}`,
        }] : [];

        if (bookRows.length || deadlineRows.length) {
          D.areas.learning.details = [...bookRows, ...deadlineRows];
          if (bk) {
            D.areas.learning.headline = bk.pct != null ? `${Math.round(bk.pct * 100)}% through` : `p. ${bk.currentPage}`;
            D.areas.learning.sub = [bk.title, deadlineRows.length && `${deadlineRows.length} due soon`].filter(Boolean).join(' · ');
          } else {
            D.areas.learning.headline = `${deadlineRows.length} due soon`;
            D.areas.learning.sub = `next · ${deadlineRows[0].k}`;
          }
          // Reading sessions drive the trend — pages/day for the last week.
          if (live.readingWeek && live.readingWeek.length > 1) {
            D.areas.learning.trend = live.readingWeek.map(r => r.pages);
          }
          L.book = bk || null;
        }
      }

      // Weather — real temp + sky in the reflection line.
      if (live.weather && typeof live.weather.temp === 'number') {
        D.greeting.weather.temp = live.weather.temp;
        D.greeting.weather.sky = live.weather.sky;
      }

      // Work — today's objectives as the card's snapshot.
      if (live.moves && live.moves.length) {
        const done = live.moves.filter(m => m.done).length;
        D.areas.work.headline = `${done} / ${live.moves.length} done`;
        D.areas.work.sub = "today's objectives";
        D.areas.work.details = live.moves.map(m => ({
          k: m.text, v: m.done ? '✓' : '·', pct: m.done ? 1 : 0,
          ...(m.id != null ? { act: { type: 'move', id: m.id } } : {}),
        }));
      }

      // Sleep — last 7 nights of the sleep habit.
      if (live.sleepWeek && live.sleepWeek.length) {
        const nights = live.sleepWeek.filter(r => r.completed).length;
        const sleepStreak = (live.habits || []).find(h => h.key === 'sleep')?.streak ?? 0;
        D.areas.sleep.headline = `${nights} of 7 nights`;
        D.areas.sleep.sub = sleepStreak > 0 ? `sleep 7+ hrs · ${sleepStreak}-night run` : 'sleep 7+ hrs · log tonight';
        D.areas.sleep.details = live.sleepWeek.map(r => ({
          k: vantaDueLabel(r.d), v: r.completed ? '✓' : '·', pct: r.completed ? 1 : 0,
        }));
        D.areas.sleep.trend = live.sleepWeek.map(r => (r.completed ? 1 : 0));
      }

      // Themes for 2026 — real yearly goals (the card shows at most four lines).
      if (live.themes && live.themes.length) {
        const four = live.themes.slice(0, 4);
        D.areas.goals.details = four.map(t => ({
          k: t.label, v: `${Math.round((t.pct || 0) * 100)}%`, goal: t.target || '—', pct: t.pct || 0,
        }));
        D.areas.goals.headline = `${four.length} theme${four.length === 1 ? '' : 's'}`;
      }

      // Real data sources for the sources screen.
      if (live.sources && live.sources.length) L.sources = live.sources;

      // The tabbed today card + the mood face read these directly.
      L.mood = typeof live.mood === 'number' ? live.mood : null;
      L.deadlines = (live.deadlines || []).map(d => ({
        title: d.title || 'untitled', course: d.course || null, due: vantaDueLabel(d.due_date), url: d.url || null,
      }));
      L.choresDue = (live.chores || []).filter(c => c.due).map(c => ({ id: c.id, label: c.label }));
      L.magentaThoughts = (live.thoughts || []).filter(t => t.area === 'magenta').slice(0, 4);

      // Home — recurring chores; rows are tappable in the detail view.
      if (live.chores && live.chores.length) {
        const due = live.chores.filter(c => c.due);
        D.areas.home.headline = due.length ? `${due.length} to do` : 'all tended';
        D.areas.home.sub = due.length
          ? due.slice(0, 3).map(c => c.label.toLowerCase()).join(' · ')
          : 'nothing due today';
        D.areas.home.details = live.chores.map(c => ({
          k: c.label, v: c.due ? 'today' : 'done', pct: c.due ? 0 : 1,
          act: { type: 'chore', id: c.id },
        }));
      }

      // Hobbies — weekly practice counts from the habit log; tap a row to
      // log today's session.
      if (live.hobbies && live.hobbies.length) {
        D.areas.hobbies.headline = `${live.hobbies.length} practices`;
        D.areas.hobbies.sub = live.hobbies.map(h => h.label.toLowerCase()).join(' · ');
        D.areas.hobbies.details = live.hobbies.map(h => ({
          k: h.label, v: `${h.count} / wk`, goal: h.goal ? String(h.goal) : '—',
          pct: h.goal ? Math.min(1, h.count / h.goal) : null,
          act: { type: 'hobby', key: h.key, count: h.count, goal: h.goal, doneToday: h.doneToday },
        }));
      }

      // Area-classified thoughts — surfaced as quiet quoted rows when the
      // matching area card expands (newest two per area).
      if (live.thoughts && live.thoughts.length) {
        const byArea = {};
        for (const t of live.thoughts) {
          if (!t.area) continue;
          (byArea[t.area] = byArea[t.area] || []).push({ text: t.text, ts: t.ts });
        }
        for (const k in byArea) byArea[k] = byArea[k].slice(0, 2);
        if (Object.keys(byArea).length) L.areaThoughts = byArea;
      }

      vantaMergeThoughts(live.thoughts);
      window.VANTA_LIVE = L;
    })
    .catch(() => {})
    .finally(() => clearTimeout(timer));
}

// Fire-and-forget DELETE, mirroring vantaPost. Fires vanta:changed for
// task deletes so the rhythm/task surfaces reconcile.
function vantaFetchDelete(path) {
  try {
    fetch(path, { method: 'DELETE' }).then(() => {
      if (/\/vanta\/(task|framework)/.test(path)) {
        window.VANTA_RHYTHM = null;
        try { window.dispatchEvent(new Event('vanta:changed')); } catch {}
      }
    }).catch(() => {});
  } catch {}
}

// Re-pull today's tasks after a write, reconciling optimistic UI to server
// truth (rollover, step→parent coupling, framework steps). Returns the fresh
// moves array (or null on failure — the caller keeps its optimistic state).
function vantaRefetchMoves() {
  return fetch('/vanta/data')
    .then(r => (r.ok ? r.json() : null))
    .then(live => {
      if (!live || !Array.isArray(live.moves)) return null;
      const moves = live.moves.map(m => ({
        id: m.id, t: m.text, text: m.text, done: m.done,
        carried: !!m.carried, steps: Array.isArray(m.steps) ? m.steps : [], area: null,
      }));
      window.VANTA_LIVE = window.VANTA_LIVE || {};
      window.VANTA_LIVE.moves = moves;
      if (live.glance) window.VANTA_LIVE.glance = live.glance;   // keep Level-0 fresh
      return moves;
    })
    .catch(() => null);
}

Object.assign(window, { vantaPost, vantaFetchDelete, vantaLoadLive, vantaNumWord, vantaDueLabel, vantaTapAct, vantaRefetchMoves });
