// Phase 2 data bootstrap.
//
// Runs BEFORE any page entry React mount. Fetches /api/* and replaces the
// template's hardcoded CHANNELS / COURSES / SIDE_COURSES / DEMO_USER /
// ADMIN_STATS globals with live data from Postgres. Entries block on
// `window.DATA_READY` via yy.waitFor.
//
// Failure mode: if any fetch errors, the template defaults (already on
// window.*) stay in place, so the page still renders. DATA_READY is set
// either way to unblock the entry.
//
// Cookie `yy_token` (set by /api/auth/signin + /api/auth/signup) carries
// auth. Fetches are same-origin so the cookie goes automatically; no
// credentials:'include' needed.

(function () {
  const TEMPLATE_COURSES_BY_ID = Object.fromEntries(
    (window.COURSES || []).map(c => [c.id, c])
  );
  const TEMPLATE_DEMO_USER = window.DEMO_USER || null;

  function relativeTime(iso) {
    if (!iso) return '';
    const t = new Date(iso).getTime();
    if (!t) return '';
    const diffDays = (Date.now() - t) / (1000 * 60 * 60 * 24);
    if (diffDays < 1) return 'today';
    if (diffDays < 2) return 'yesterday';
    if (diffDays < 8) return Math.floor(diffDays) + ' days ago';
    if (diffDays < 30) return Math.floor(diffDays / 7) + ' weeks ago';
    return new Date(t).toISOString().slice(0, 10);
  }

  function joinedMonth(iso) {
    if (!iso) return '';
    const d = new Date(iso);
    if (isNaN(d.getTime())) return String(iso);
    return d.toLocaleString('en', { month: 'long' }) + ' ' + d.getFullYear();
  }

  async function fetchJson(url, opts) {
    const r = await fetch(url, opts || {});
    if (!r.ok) throw new Error(url + ' -> ' + r.status);
    return r.json();
  }

  // Retry once on 5xx — Supabase's pooler occasionally drops idle connections,
  // and the first request after the drop fails. The retry hits a fresh socket.
  async function fetchJsonRetry(url) {
    try {
      return await fetchJson(url);
    } catch (e) {
      if (/-> 5\d\d/.test(String(e.message || e))) {
        await new Promise(r => setTimeout(r, 150));
        return fetchJson(url);
      }
      throw e;
    }
  }

  function currentLocale() {
    return (typeof localStorage !== 'undefined' && localStorage.getItem('yylearn.lang')) ||
      (document.documentElement.lang || '').split('-')[0] || 'en';
  }

  function applyChannelsResult(chOut) {
    if (chOut.status === 'fulfilled' && chOut.value && Array.isArray(chOut.value.channels) && chOut.value.channels.length) {
      window.CHANNELS = chOut.value.channels;
    } else if (chOut.status === 'rejected') {
      console.warn('[yy] /api/channels failed, keeping template CHANNELS:', chOut.reason);
    }
  }

  function applyCoursesResult(coOut) {
    if (coOut.status === 'fulfilled' && coOut.value && Array.isArray(coOut.value.courses) && coOut.value.courses.length) {
      // If the DB row lacks lessons_full but a template course with the same
      // id has it populated, fall back to the template lesson list so the
      // player page keeps working. (Phase 3 can move lesson data into DB.)
      const merged = coOut.value.courses.map(c => {
        if (!c.lessons_full || !c.lessons_full.length) {
          const tpl = TEMPLATE_COURSES_BY_ID[c.id];
          if (tpl && tpl.lessons_full && tpl.lessons_full.length) {
            return Object.assign({}, c, { lessons_full: tpl.lessons_full });
          }
        }
        return c;
      });
      window.COURSES = merged;
      // SIDE_COURSES: template expects instructor as a string.
      window.SIDE_COURSES = merged.map(c => ({
        id: c.id,
        channel: c.channel,
        title: c.title,
        hours: c.hours,
        lessons: c.lessons,
        students: c.students,
        price: c.price,
        seal: c.seal,
        instructor: (c.instructor && c.instructor.name) || (typeof c.instructor === 'string' ? c.instructor : ''),
      }));
    } else if (coOut.status === 'rejected') {
      console.warn('[yy] /api/courses failed, keeping template COURSES:', coOut.reason);
    }
  }

  // Kick off public-data fetches (channels + courses) as parallel promises
  // without awaiting. Returns the two promises so bootstrap() can await them
  // alongside /api/auth/me — flattening the waterfall from 3 round-trips to 2.
  function startPublicFetches() {
    const q = '?locale=' + encodeURIComponent(currentLocale());
    return {
      channels: fetchJsonRetry('/api/channels' + q).then(v => ({ status: 'fulfilled', value: v }), r => ({ status: 'rejected', reason: r })),
      courses: fetchJsonRetry('/api/courses' + q).then(v => ({ status: 'fulfilled', value: v }), r => ({ status: 'rejected', reason: r })),
    };
  }

  async function loadUserData(user) {
    if (!user) return null;

    const [enrOut, ordOut] = await Promise.allSettled([
      fetchJson('/api/enrollments'),
      fetchJson('/api/orders'),
    ]);
    const enrollments = (enrOut.status === 'fulfilled' && enrOut.value && enrOut.value.enrollments) || {};
    const orders = (ordOut.status === 'fulfilled' && ordOut.value && Array.isArray(ordOut.value.orders)) ? ordOut.value.orders : [];

    const enrArr = Object.values(enrollments);
    const coursesById = Object.fromEntries((window.COURSES || []).map(c => [c.id, c]));
    const hoursWatched = Math.round(
      enrArr.reduce((s, e) => {
        const co = coursesById[e.courseId];
        if (!co) return s;
        return s + (Number(co.hours) || 0) * ((Number(e.progress) || 0) / 100);
      }, 0)
    );
    const certs = enrArr.filter(e => e.completed).length;

    const fallbackStats = (TEMPLATE_DEMO_USER && TEMPLATE_DEMO_USER.stats) || { streak: 0 };
    const fallbackMembership = (TEMPLATE_DEMO_USER && TEMPLATE_DEMO_USER.membership) || { tier: 'free' };

    window.DEMO_USER = {
      name: user.name,
      email: user.email,
      avatar: user.avatar || String(user.name || '?').slice(0, 2).toUpperCase(),
      country: user.country,
      joined: joinedMonth(user.joined || user.createdAt),
      membership: Object.assign({}, fallbackMembership, user.membership || {}),
      stats: {
        courses: enrArr.length,
        hours: hoursWatched,
        streak: fallbackStats.streak || 0,
        certs,
      },
      enrolled: enrArr.map(e => ({
        courseId: e.courseId,
        progress: Number(e.progress) || 0,
        lastLesson: Number(e.lastLesson) || 1,
        lastSeen: relativeTime(e.lastSeen || e.enrolledAt),
        completed: !!e.completed,
      })),
      orders: orders.map(o => ({
        id: o.id,
        date: (o.createdAt || o.date || '').slice(0, 10),
        item: o.item,
        amount: Number(o.amount) || 0,
        status: o.status,
      })),
    };

    // Make the signed-in user visible to page entries that check __DATA__.user
    // (e.g. home-entry.signedIn). Server-side already sets this for SSR, but
    // cookie-only flows still need it.
    if (window.__DATA__ && !window.__DATA__.user) window.__DATA__.user = user;

    // If the signed-in user has a DB-stored locale or theme preference that
    // differs from what's currently active (localStorage), adopt the DB value.
    // Signin/signup already sets cookies server-side, so this mainly catches
    // "JWT cookie survived but pref cookies didn't" edge cases and keeps the
    // on-screen theme selector reflecting reality.
    try {
      const storedTheme = localStorage.getItem('yylearn.theme');
      if (user.themePref && user.themePref !== storedTheme) {
        localStorage.setItem('yylearn.theme', user.themePref);
        document.cookie = 'yy_theme=' + encodeURIComponent(user.themePref) + '; path=/; max-age=31536000';
        if (typeof applyTheme === 'function') applyTheme(user.themePref);
      }
    } catch (_) { /* best-effort */ }

    return user;
  }

  // Lightweight /api/auth/me — returns user object or null. Kicked off in
  // parallel with public fetches to flatten the cold-load waterfall.
  async function fetchMe() {
    try {
      const r = await fetch('/api/auth/me');
      if (r.status === 401) return null;
      if (!r.ok) throw new Error('/api/auth/me -> ' + r.status);
      const j = await r.json();
      return j.user || null;
    } catch (e) {
      console.warn('[yy] /api/auth/me failed:', e);
      return null;
    }
  }

  async function loadAdminStats(user) {
    if (!user || user.role !== 'admin') return;
    try {
      const stats = await fetchJson('/api/admin/stats');
      if (stats && typeof stats === 'object') window.ADMIN_STATS = stats;
    } catch (e) {
      console.warn('[yy] /api/admin/stats failed:', e);
    }
  }

  async function bootstrap() {
    try {
      // Fire all independent fetches in parallel — channels, courses, and
      // /api/auth/me have no mutual dependencies. Previous version awaited
      // public data first, then /me, then enrollments+orders — three waves.
      // Now: {channels, courses, me} in wave 1; {enrollments, orders} in
      // wave 2. On cold load (200-300ms RTT per request), this cuts the
      // bootstrap time roughly in half.
      const publicFetches = startPublicFetches();
      const mePromise = fetchMe();

      const [chOut, coOut, user] = await Promise.all([
        publicFetches.channels,
        publicFetches.courses,
        mePromise,
      ]);

      applyChannelsResult(chOut);
      applyCoursesResult(coOut);

      // loadUserData depends on window.COURSES (for hoursWatched), which is
      // now populated — run it after the Promise.all.
      await loadUserData(user);

      // Admin stats are nice-to-have — don't block DATA_READY on them. Fire
      // and forget; worst case ADMIN_STATS keeps the template fallback.
      loadAdminStats(user);
    } catch (e) {
      console.warn('[yy] bootstrap error:', e);
    } finally {
      window.DATA_READY = true;
      try { document.dispatchEvent(new Event('yy:data-ready')); } catch (_) {}
    }
  }

  bootstrap();
})();
