/* =========================================================
   Tappt dashboard — root router + mount
   ========================================================= */
const { Auth, Onboarding, Dashboard, Store: RStore, useToast } = window;
const { useState: rUseState, useEffect: rUseEffect } = React;

/* Remember where the user last tapped so the theme toggle's circular reveal
   (View Transitions) can expand from that point. */
if (typeof window !== 'undefined' && !window.__themeXYbound) {
  window.__themeXYbound = true;
  window.addEventListener('pointerdown', (e) => {
    window.__themeXY = { x: e.clientX, y: e.clientY };
  }, true);
}

/* Baked-in default profile so @maxxharland never loses his login during the demo.
   If there's no saved session, we seed this and drop straight into the dashboard. */
const DEFAULT_USER = {
  email: 'max@tappt.io', name: 'Max Harland', handle: 'maxxharland',
  bio: 'Founder · Tappt · York', avatarColor: 'linear-gradient(135deg,#6B8299,#3a4f63)',
  activeCard: 'joker', onboarded: true, pro: true, followers: '12.4K',
  align: 'centered', actions: [], merchLayout: 'carousel', merchCard: 'card', merchRotate: true,
  socials: [],
  sections: [
    { id: 'bio', label: 'Bio', on: true },
    { id: 'featured', label: 'Featured Links', on: false },
    { id: 'embeds', label: 'Embeds', on: false },
    { id: 'moments', label: 'Moments', on: true },
    { id: 'merch', label: 'Merch & Products', on: true },
    { id: 'gallery', label: 'Gallery', on: false },
  ],
  links: [],
  products: [
    { name: 'Joker', price: '£29.97', tag: 'Tappt Card', img: 'assets/cards/joker-front.png', imgKind: 'image', badge: 'LIMITED', link: 'tappt.io/shop/joker', desc: 'NFC smart card · tap to share your Tappt profile.' },
    { name: 'American Success', price: '£29.97', tag: 'Tappt Card', img: 'assets/cards/americansuccess-front.png', imgKind: 'image', badge: '', link: 'tappt.io/shop/americansuccess', desc: 'NFC smart card · tap to share your Tappt profile.' },
    { name: 'Lover Girl', price: '£29.97', tag: 'Tappt Card', img: 'assets/cards/lovergirl-front.png', imgKind: 'image', badge: '', link: 'tappt.io/shop/lovergirl', desc: 'NFC smart card · tap to share your Tappt profile.' },
    { name: 'Unbothered', price: '£29.97', tag: 'Tappt Card', img: 'assets/cards/unbothered-front.png', imgKind: 'image', badge: '', link: 'tappt.io/shop/unbothered', desc: 'NFC smart card · tap to share your Tappt profile.' },
    { name: 'Lucky Duck', price: '£29.97', tag: 'Tappt Card', img: 'assets/cards/luckyduck-front.png', imgKind: 'image', badge: '', link: 'tappt.io/shop/luckyduck', desc: 'NFC smart card · tap to share your Tappt profile.' },
    { name: 'Black', price: '£29.97', tag: 'Tappt Card', img: 'assets/cards/black-front.png', imgKind: 'image', badge: 'PRO', link: 'tappt.io/shop/black', desc: 'NFC smart card · tap to share your Tappt profile.' },
    { name: 'Desert', price: '£29.97', tag: 'Tappt Card', img: 'assets/cards/desert-front.png', imgKind: 'image', badge: 'PRO', link: 'tappt.io/shop/desert', desc: 'NFC smart card · tap to share your Tappt profile.' },
  ],
};

/* DEMO_USER stays exported for any code that references it, but we DO NOT auto-seed a
   session anymore — a fresh visitor (no saved user, no Supabase token) must land on the
   login screen. Real logins are restored by the Supabase-session effect below; a returning
   user's own saved session persists via RStore. */
window.DEMO_USER = DEFAULT_USER;

function App() {
  // phase: 'auth' | 'onboard' | 'dash'. We NEVER trust localStorage to decide the phase
  // anymore — the Supabase session is the single source of truth (decided in the boot effect
  // below). Start blank; show a splash while we check who's actually signed in.
  const [user, setUser] = rUseState(null);
  const [phase, setPhase] = rUseState('auth');
  const [booting, setBooting] = rUseState(() => {
    try { return !!window.__tapptOAuthBusy || RStore.hasSession() || /[#&]access_token=|[?&]code=/.test((location.hash || '') + (location.search || '')); } catch (e) { return false; }
  });
  const [themeMode, setThemeMode] = rUseState(() => localStorage.getItem('tappt-theme-mode') || 'dark');
  const [toast, toastNode] = useToast();
  // True when this page load is the return leg of a Google/Apple redirect (tokens in the URL).
  const [oauthBusy, setOauthBusy] = rUseState(() => {
    try { return !!window.__tapptOAuthBusy || /[#&]access_token=|[?&]code=/.test((location.hash || '') + (location.search || '')); } catch (e) { return false; }
  });

  /* ───────────────────────────────────────────────────────────────────────────
     BOOT — Supabase session is authoritative (the LinkMe-standard flow):
       • signed in + completed profile  → dashboard (cloud data, never demo)
       • signed in + no/blank handle     → onboarding
       • NOT signed in                   → login, and we WIPE any stale local cache
                                           so a previous account's / the demo's data
                                           can never leak to the next person.
     Runs once on mount, ignores localStorage for routing.
     ─────────────────────────────────────────────────────────────────────────── */
  rUseEffect(() => {
    let alive = true;
    let handled = false;
    const settle = (ph, u) => {
      if (!alive || handled) return;
      handled = true;
      if (u !== undefined) setUser(u);
      setPhase(ph);
      setBooting(false);
      setOauthBusy(false);
      try { if (/[#&]access_token=|[?&]code=/.test((location.hash || '') + (location.search || ''))) history.replaceState(null, '', '/login'); } catch (e) {}
    };
    const route = (cu, prof) => {
      const realHandle = !!(prof && prof.handle && !/^user_[0-9a-f]{8}$/.test(prof.handle));
      const isDone = !!(prof && prof.handle && (prof.onboarded === true || realHandle));
      if (isDone) {
        const u = RStore.migrateColors ? RStore.migrateColors(Object.assign({}, prof)) : Object.assign({}, prof);
        RStore.cacheUser(u);
        settle('dash', u);
      } else {
        // brand-new / not-yet-onboarded (incl. OAuth): blank slate, no demo, no trigger-filled
        // name/handle — they pick those in onboarding.
        const u = Object.assign({}, prof || {}, { email: cu.email, name: '', handle: '', onboarded: false, links: (prof && prof.links) || [] });
        RStore.cacheUser(u);
        settle('onboard', u);
      }
    };
    const boot = async () => {
      // wait for supabase-js to load (scripts are async)
      let tries = 0;
      while (alive && (!window.TapptDB || !window.TapptDB.available()) && tries < 80) {
        await new Promise((r) => setTimeout(r, 120)); tries++;
      }
      if (!alive) return;
      if (!window.TapptDB || !window.TapptDB.available()) {
        // supabase never loaded (offline/CDN down) — fall back to a cached local user if any
        const local = RStore.getUser();
        if (local && local.handle && RStore.hasSession()) settle(local.onboarded ? 'dash' : 'onboard', local);
        else { RStore.clearLocalUser(); settle('auth', null); }
        return;
      }
      // make sure any OAuth hash is consumed + session persisted first
      if (oauthBusy || /[#&]access_token=|[?&]code=/.test((location.hash || '') + (location.search || ''))) {
        try { await window.__tapptOAuth; } catch (e) {}
      }
      // who is ACTUALLY signed in (reads the persisted session, no network)
      let cu = null;
      try { cu = await window.TapptDB.sessionUser(); } catch (e) {}
      if (!cu) { RStore.clearLocalUser(); settle('auth', null); return; }
      let prof = null;
      try { prof = await window.TapptDB.loadProfile(cu.id); } catch (e) {}
      route(cu, prof);
    };
    boot();
    // belt-and-braces: if a session lands late (async OAuth), catch it while still on login
    let sub = null;
    if (window.TapptDB && window.TapptDB.onAuth) {
      sub = window.TapptDB.onAuth((cu) => {
        if (!alive || handled || !cu) return;
        window.TapptDB.loadProfile(cu.id).then((prof) => route(cu, prof)).catch(() => route(cu, null));
      });
    }
    return () => { alive = false; if (sub && sub.unsubscribe) { try { sub.unsubscribe(); } catch (e) {} } };
  }, []);

  /* hydrate big media (video cover etc.) from IndexedDB after first paint, then pull the
     signed-in user's moments + collections from their dedicated tables. The PROFILE itself is
     loaded authoritatively in the boot effect above, so we don't re-merge it here. */
  rUseEffect(() => {
    let alive = true;
    const raw = RStore.getUser();
    if (raw && JSON.stringify(raw).indexOf('idb:') !== -1 && RStore.hydrateMedia) {
      RStore.hydrateMedia(JSON.parse(JSON.stringify(raw))).then((h) => { if (alive) setUser(h); });
    }
    // pull the signed-in user's moments from their dedicated table
    if (RStore.loadMoments) {
      RStore.loadMoments().then((moms) => {
        if (alive && moms) setUser((cur) => Object.assign({}, cur, { moments: moms }));
      });
    }
    // pull connections / leads / tips / cards from their dedicated tables
    if (RStore.loadCollections) {
      RStore.loadCollections().then((cols) => {
        if (alive && cols) setUser((cur) => Object.assign({}, cur, cols));
      });
    }
    return () => { alive = false; };
  }, []);

  /* finish a card claim that started logged-out (claim.html bounced here with ?claim=<code>).
     Wait until the user is fully onboarded (they need a persona to aim the card at), then
     claim it server-side, register it locally, clean the URL, and route to the live profile. */
  rUseEffect(() => {
    if (phase !== 'dash') return;
    let code = null;
    // URL param (email signup keeps it) OR localStorage (survives the OAuth round-trip, which
    // returns to a clean /login and would otherwise drop ?claim=).
    try { code = new URLSearchParams(location.search).get('claim'); } catch (e) {}
    if (!code) { try { code = localStorage.getItem('tappt_pending_claim'); } catch (e) {} }
    if (!code) return;
    let alive = true;
    (async () => {
      try {
        const personas = (window.getPersonas ? window.getPersonas(user) : (user.personas || [])) || [];
        const pid = (personas[0] && personas[0].id) || null;
        if (window.TapptDB && window.TapptDB.available()) {
          await window.TapptDB.claimCard(code, pid);
        }
        if (!alive) return;
        // register the card locally so My Cards shows it immediately
        const card = { id: window.newId ? window.newId() : ('c' + Date.now()), code: code, persona: pid, status: 'delivered', paired: true, claimed: true, label: 'Tappt Card' };
        const cur = RStore.getUser() || user;
        const cards = [card].concat((cur.cards || []).filter((c) => c.code !== code));
        const next = Object.assign({}, cur, { cards: cards });
        RStore.saveUser(next);
        if (RStore.saveCard) { try { RStore.saveCard(card); } catch (e) {} }
        setUser(next);
        toast('Card claimed — it now opens your profile');
      } catch (e) {
        toast((e && e.message) === 'already_claimed' ? 'That card is already claimed by another account.' : 'Could not claim that card — try again from the card link.');
      } finally {
        // strip ?claim= + clear the pending-claim stash so a refresh doesn't re-run it
        try { localStorage.removeItem('tappt_pending_claim'); } catch (e) {}
        try { const u = new URL(location.href); u.searchParams.delete('claim'); history.replaceState(null, '', u.pathname + u.search + u.hash); } catch (e) {}
      }
    })();
    return () => { alive = false; };
  }, [phase]);

  /* stash ?claimprofile=<token> so it survives the OAuth round-trip. CRITICAL SAFETY:
     - sessionStorage (NOT localStorage) so it can NEVER persist across browser sessions and
       get applied to an unrelated future login (that bug merged an influencer profile into a
       real account days later).
     - timestamped; only honoured for 30 min.
     - we also purge any LEGACY localStorage keys on every boot to clean already-affected
       devices. */
  rUseEffect(() => {
    try { localStorage.removeItem('tappt_pending_pregen'); localStorage.removeItem('tappt_pregen_apply'); } catch (e) {}
    try {
      const t = new URLSearchParams(location.search).get('claimprofile');
      if (t) {
        sessionStorage.setItem('tappt_pending_pregen', JSON.stringify({ token: t, ts: Date.now() }));
        try { const u = new URL(location.href); u.searchParams.delete('claimprofile'); history.replaceState(null, '', u.pathname + u.search + u.hash); } catch (e) {}
      }
    } catch (e) {}
  }, []);

  /* finish a pre-generated profile claim — ONLY for a brand-new account that arrived via a
     fresh claim link THIS session. onOnboarded arms `tappt_pregen_apply`; we require the
     stash to exist, be <30 min old, and the apply-arm to match. Never touches an established
     profile. */
  rUseEffect(() => {
    if (phase !== 'dash') return;
    let token = null;
    try {
      const raw = sessionStorage.getItem('tappt_pending_pregen');
      if (raw) { const o = JSON.parse(raw); if (o && o.token && (Date.now() - (o.ts || 0)) < 1800000) token = o.token; }
    } catch (e) {}
    let apply = false;
    try { apply = !!token && sessionStorage.getItem('tappt_pregen_apply') === token; } catch (e) {}
    if (!token || !apply) return;
    let alive = true;
    (async () => {
      try {
        if (!window.TapptDB || !window.TapptDB.available()) return;
        const res = await window.TapptDB.claimPregen(token);
        if (!alive || !res || res.error) {
          if (res && res.error === 'claimed') toast('That profile was already claimed.');
          return;
        }
        const data = res.data || {};
        const cur = RStore.getUser() || user;
        // merge the pre-built design into their account; keep their email/id/personas
        const next = Object.assign({}, cur, data, {
          name: res.name || cur.name,
          bio: res.bio || cur.bio,
          handle: res.handle || cur.handle,
          onboarded: true,
        });
        RStore.saveUser(next);
        // persist the seeded moment to the moments table so it survives cross-device
        try {
          if (RStore.saveMoment && data.moments && data.moments.length) { data.moments.forEach((m) => RStore.saveMoment(m)); }
        } catch (e) {}
        setUser(next);
        toast('Your Tappt profile is ready ✨');
      } catch (e) {
        toast('Could not load that profile — try the link again.');
      } finally {
        try { sessionStorage.removeItem('tappt_pending_pregen'); sessionStorage.removeItem('tappt_pregen_apply'); } catch (e) {}
      }
    })();
    return () => { alive = false; };
  }, [phase]);

  rUseEffect(() => {
    localStorage.setItem('tappt-theme-mode', themeMode);
    const apply = () => {
      let t = themeMode;
      if (themeMode === 'auto') t = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
      const root = document.documentElement;
      if (root.dataset.theme === t) return;          // no actual change
      const swap = () => { root.dataset.theme = t; };
      const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      // Magic-UI "Animated Theme Toggler": circular reveal from the tap point
      if (!document.startViewTransition || reduce) { swap(); return; }
      const p = window.__themeXY || { x: window.innerWidth - 40, y: 44 };
      const endR = Math.hypot(Math.max(p.x, window.innerWidth - p.x), Math.max(p.y, window.innerHeight - p.y));
      const vt = document.startViewTransition(swap);
      vt.ready.then(() => {
        document.documentElement.animate(
          { clipPath: [`circle(0px at ${p.x}px ${p.y}px)`, `circle(${endR}px at ${p.x}px ${p.y}px)`] },
          { duration: 520, easing: 'cubic-bezier(.4,0,.2,1)', pseudoElement: '::view-transition-new(root)' }
        );
      }).catch(() => {});
    };
    apply();
    if (themeMode === 'auto') {
      const mq = window.matchMedia('(prefers-color-scheme: dark)');
      mq.addEventListener('change', apply);
      return () => mq.removeEventListener('change', apply);
    }
  }, [themeMode]);

  rUseEffect(() => {
    // track the visual viewport so bottom sheets sit above the iOS keyboard
    // (fixes the whole-frame "shift" when an input is focused)
    const vv = window.visualViewport;
    if (!vv) return;
    const sync = () => {
      const kb = Math.max(0, (window.innerHeight - vv.height - vv.offsetTop));
      document.documentElement.style.setProperty('--kb', kb + 'px');
      document.documentElement.style.setProperty('--vvh', vv.height + 'px');
    };
    sync();
    vv.addEventListener('resize', sync);
    vv.addEventListener('scroll', sync);
    return () => { vv.removeEventListener('resize', sync); vv.removeEventListener('scroll', sync); };
  }, []);

  const onAuthed = (u, isNew) => {
    RStore.cacheUser(u);
    setUser(u);
    setBooting(false); setOauthBusy(false);
    setPhase(isNew || !u.onboarded ? 'onboard' : 'dash');
  };
  const onOnboarded = (u) => {
    // a pregen claim only applies to a JUST-created account — arm it here (sessionStorage so it
    // can't leak to a future login). The dash hook runs it once, then clears it.
    try {
      const raw = sessionStorage.getItem('tappt_pending_pregen');
      if (raw) { const o = JSON.parse(raw); if (o && o.token) sessionStorage.setItem('tappt_pregen_apply', o.token); }
    } catch (e) {}
    setUser(u); setThemeMode('dark'); setPhase('dash'); toast('Profile is live');
    // Guarantee the profile reaches the cloud — retry a few times, and if it genuinely
    // can't (no session), say so instead of silently leaving a half-made account that
    // bounces back to onboarding on next login.
    (async () => {
      for (let i = 0; i < 4; i++) {
        let ok = false;
        try { ok = await RStore.syncRemote(u); } catch (e) {}
        if (ok) return;
        await new Promise((r) => setTimeout(r, 600 * (i + 1)));
      }
      toast("Couldn't save to the cloud — check connection");
    })();
  };
  const onLogout = () => { RStore.logout(); setUser(null); setBooting(false); setOauthBusy(false); setPhase('auth'); };

  return (
    <React.Fragment>
      {phase === 'auth' && (oauthBusy || booting) && (
        <div className="oauth-splash">
          <div className="oauth-splash-mark">
            <img src="assets/icons/icon-mark-bw.png" alt="" width="48" height="48" />
          </div>
          <div className="oauth-splash-spin" aria-hidden="true"></div>
          <div className="oauth-splash-txt">Signing you in…</div>
        </div>
      )}
      {phase === 'auth' && !oauthBusy && !booting && <Auth onAuthed={onAuthed} themeMode={themeMode} setThemeMode={setThemeMode} />}
      {phase === 'onboard' && <Onboarding user={user} onDone={onOnboarded} themeMode={themeMode} setThemeMode={setThemeMode} />}
      {phase === 'dash' && <Dashboard user={user} setUser={setUser} onLogout={onLogout} themeMode={themeMode} setThemeMode={setThemeMode} toast={toast} />}
      {toastNode}
    </React.Fragment>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
