// 'onboarding' MUST be in this list. It is a real page in the renderer below, but it used to be
// reachable only by an in-app go('onboarding') from the Hero / SubscribeCTA — so a direct
// https://readsynth.ca/#onboarding fell through to 'home'. The welcome email's primary CTA is
// exactly that URL, which would have landed every reader on the homepage (2026-09-09).
// 'privacy' and 'terms' are the interim legal pages (PR #4). All four route additions must
// coexist here — dropping any one silently re-breaks that route.
// 'manage' is the self-service "Manage My Preferences" route (the newsletter footer links here).
// It renders the SAME OnboardingPage in edit mode — seeded from the subscriber's current prefs
// (browser-remembered email, else the wizard asks once), saving via the same /signup upsert.
const HASH_PAGES = ['home','product','changing','faq','team','newsletters','onboarding','manage','privacy','terms'];
// Per-viewer convenience: remember the email a signup used on THIS browser, so a return visit to
// #manage prefills without typing. localStorage only (per-device, never shared/never reaches us);
// every access is wrapped because private mode / blocked storage throws. Not identity — a mere
// convenience; #manage still works (asks for the email) when it is absent.
const REMEMBER_KEY = 'synth_email';
function rememberEmail(email) {
  try { if (email) localStorage.setItem(REMEMBER_KEY, email); } catch (e) { /* storage blocked */ }
}
function rememberedEmail() {
  try { return localStorage.getItem(REMEMBER_KEY) || ''; } catch (e) { return ''; }
}
// Exposed so OnboardingPage (a separately-loaded global script) can remember the email on a
// successful submit without an import.
window.synthRememberEmail = rememberEmail;

// Ad attribution (ads-readiness #4). Capture utm_source/medium/campaign/content from the landing
// URL ONCE on load — before any SPA navigation rewrites location.search — so a signup records which
// ad it came from (stored in preferences.utm). Exposed as a global for OnboardingPage to read into
// its /lead and /signup payloads. Empty object when there are no utm params. Never throws.
window.synthUtm = (function () {
  try {
    var sp = new URLSearchParams(location.search);
    var out = {};
    ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(function (k) {
      var val = sp.get(k);
      if (val) out[k] = String(val).slice(0, 120);   // cap length defensively
    });
    return out;
  } catch (e) { return {}; }
})();
function initialPage() {
  const h = (location.hash || '').replace('#','');
  return HASH_PAGES.indexOf(h) > -1 ? h : 'home';
}
// Email prefill. The wizard already accepts a seedEmail, but nothing ever read one from the URL,
// so an emailed "choose your teams" link made the reader retype the address we already knew — and
// the /lead capture in OnboardingPage is gated on a valid email, so it never fired either.
//
// The query must sit BEFORE the fragment (/?email=x#onboarding). The SPA is hash-routed, so in
// /#onboarding?email=x the query is part of the FRAGMENT and location.search is empty; we read the
// hash form too rather than silently dropping a link written that way.
function initialSeedEmail() {
  try {
    var q = new URLSearchParams(location.search).get('email');
    if (!q) {
      var h = location.hash || '';
      var i = h.indexOf('?');
      if (i > -1) q = new URLSearchParams(h.slice(i + 1)).get('email');
    }
    // Only accept something email-shaped. A junk value would put the wizard's final step into a
    // valid-looking state it cannot submit.
    if (!(q && /.+@.+\..+/.test(q))) return '';
    // Strip `email` from the address bar once we've read it (Will, 2026-10-01): it came from a link
    // in their inbox, so it should not linger in browser history or a copy-pasted URL. Scrub it from
    // wherever it rode in — location.search (our welcome CTA's /?email=x#hash form) or the hash's own
    // query (/#onboarding?email=x) — preserving every OTHER param, and replace in place (no new
    // history entry). Best-effort: if replaceState is unavailable the email simply stays in the bar.
    try {
      var sp = new URLSearchParams(location.search);
      var newHash = location.hash || '';
      var hi = newHash.indexOf('?');
      if (hi > -1) {
        var hp = new URLSearchParams(newHash.slice(hi + 1));
        if (hp.has('email')) { hp.delete('email'); var hq = hp.toString();
          newHash = newHash.slice(0, hi) + (hq ? '?' + hq : ''); }
      }
      if (sp.has('email') || hi > -1) {
        sp.delete('email');
        var qs = sp.toString();
        history.replaceState(null, '', location.pathname + (qs ? '?' + qs : '') + newHash);
      }
    } catch (e2) { /* replaceState blocked/unsupported — harmless */ }
    return q.trim();
  } catch (e) {
    return '';   // malformed URI, ancient browser — the wizard still works, just unseeded
  }
}
// Newsletter archive data, loaded ON DEMAND (ads-readiness #2). newsletter_data_2.js is ~15 MB of JS
// (~1.6 MB over the wire) that only the Newsletters tab and the ?card_export view read, so the
// homepage must never fetch it. index.hbs sets window.SYNTH_DATA_SRC to the file's name; deploy_site's
// content-hash step rewrites that quoted string to the hashed filename, and a relative src resolves
// against <base href="/assets/site/"> like every other asset. The dev shells set nothing and fall
// back to the plain name. export.html (the email image render) still loads the file SYNCHRONOUSLY,
// so there window.SYNTH_ISSUES already exists and this resolves at once — keep it that way.
// A stalled request (flaky mobile link, captive portal) fires neither onload nor onerror, so give
// up after this long and offer Try again. Generous: 1.6 MB takes ~50 s even on slow 2G. A tag that
// timed out is left in place — if it lands later, the retry finds window.SYNTH_ISSUES and is instant.
const DATA_LOAD_TIMEOUT_MS = 90000;
let newsletterDataPromise = null;
let newsletterDataAttempts = 0;
function loadNewsletterData() {
  if (window.SYNTH_ISSUES) return Promise.resolve();
  if (newsletterDataPromise) return newsletterDataPromise;
  newsletterDataAttempts += 1;
  const attempt = new Promise((resolve, reject) => {
    const s = document.createElement('script');
    const src = window.SYNTH_DATA_SRC || 'newsletter_data_2.js';
    // A retry must use a URL of its own: the browser attaches a second request for the SAME URL to
    // the one still stalled in flight, so a plain re-inject never reaches the network. First loads
    // keep the canonical hashed URL so the CDN and browser cache serve them.
    s.src = newsletterDataAttempts > 1
      ? src + (src.indexOf('?') > -1 ? '&' : '?') + 'retry=' + newsletterDataAttempts
      : src;
    let settled = false;
    const settle = (ok, why, dropTag) => {
      if (settled) return;            // a late onload after a timeout must not settle twice
      settled = true;
      clearTimeout(timer);
      if (ok) return resolve();
      if (dropTag) s.remove();
      if (newsletterDataPromise === attempt) newsletterDataPromise = null;  // let a retry start fresh
      reject(new Error(why));
    };
    const timer = setTimeout(() => settle(false, 'data file timed out', false), DATA_LOAD_TIMEOUT_MS);
    // 200 is not enough: a gated (private-mode) site answers an asset with login HTML.
    s.onload = () => (window.SYNTH_ISSUES ? settle(true)
                                         : settle(false, 'data file did not define SYNTH_ISSUES', true));
    s.onerror = () => settle(false, 'data file failed to load', true);
    document.body.appendChild(s);
  });
  newsletterDataPromise = attempt;
  return attempt;
}
window.synthLoadNewsletterData = loadNewsletterData;
// Gate for the Newsletters tab. NewslettersPage reads the data globals in its useState initialisers,
// so it must not mount until they exist — mounting early would freeze empty dropdowns.
function NewslettersRoute({ onNav }) {
  const [status, setStatus] = React.useState(window.SYNTH_ISSUES ? 'ready' : 'loading');
  const load = () => {
    setStatus(window.SYNTH_ISSUES ? 'ready' : 'loading');
    loadNewsletterData().then(() => setStatus('ready'), () => setStatus('error'));
  };
  React.useEffect(load, []);
  if (status === 'ready') return <NewslettersPage onNav={onNav} />;
  // Same look as NewslettersPage's "No issue found" line (its NL.mono / NL.ink3), spelled out here so
  // this fallback doesn't depend on another script's private constant.
  const line = { textAlign:'center', fontFamily:"'IBM Plex Mono', monospace", color:'#566078', padding:'60px 0' };
  return (
    <main style={{ background:'var(--syn-paper, #FBF7F1)', minHeight:'calc(100vh - 64px)' }}>
      {status === 'loading'
        ? <div style={line}>Loading newsletters…</div>
        : <div style={line}>
            Couldn't load newsletters. Check your connection.{' '}
            <button type="button" onClick={load} style={{ font:'inherit', color:'inherit', background:'none',
              border:'none', padding:0, textDecoration:'underline', cursor:'pointer' }}>Try again</button>
          </div>}
    </main>
  );
}
function Site() {
  const [page, setPage] = React.useState(initialPage);
  const [shown, setShown] = React.useState(initialPage);
  const [op, setOp] = React.useState(1);
  const [seedEmail, setSeedEmail] = React.useState(initialSeedEmail);
  const go = (p, email) => {
    if (typeof email === 'string') setSeedEmail(email);
    if (p === page) return;
    // Start the archive download now so it overlaps the 240ms fade; NewslettersRoute picks up the
    // same promise (or retries if this one has already failed).
    if (p === 'newsletters') loadNewsletterData().catch(() => {});
    setPage(p);
    setOp(0);
    setTimeout(() => { setShown(p); window.scrollTo(0, 0); setOp(1); }, 240);
  };
  // Respond to the URL changing after mount: pasting /#privacy into the address bar,
  // and browser back/forward. Without this `initialPage()` only runs on a cold load, so
  // a hash change would leave the app showing the previous page. No dep array — the
  // listener is cheap to re-register and this always closes over the current `go`
  // (which early-returns when the page is unchanged, so this cannot loop).
  React.useEffect(() => {
    const onHash = () => {
      const h = (location.hash || '').replace('#', '');
      go(HASH_PAGES.indexOf(h) > -1 ? h : 'home');
    };
    window.addEventListener('hashchange', onHash);
    window.addEventListener('popstate', onHash);
    return () => {
      window.removeEventListener('hashchange', onHash);
      window.removeEventListener('popstate', onHash);
    };
  });

  const dark = page === 'team' || page === 'product';
  const page5 = shown;
  return (
    <div style={{
      minHeight: '100vh',
      backgroundColor: dark ? 'var(--syn-navy)' : 'var(--syn-paper)',
      transition: 'background-color 520ms cubic-bezier(0.2,0.7,0.2,1)',
    }}>
      <Nav dark={dark} page={page} onNav={go} />
      <div style={{ opacity: op, transition: 'opacity 240ms cubic-bezier(0.2,0.7,0.2,1)' }}>
        {page5 === 'home' ? (
          <React.Fragment>
            <Hero onStart={email => go('onboarding', email)} />
            <SubscribeCTA onStart={email => go('onboarding', email)} />
          </React.Fragment>
        ) : page5 === 'product' ? (
          <ProductPage onNav={go} />
        ) : page5 === 'changing' ? (
          <ChangingPage onNav={go} />
        ) : page5 === 'faq' ? (
          <FAQPage onNav={go} />
        ) : page5 === 'onboarding' ? (
          <OnboardingPage seedEmail={seedEmail} onNav={go} />
        ) : page5 === 'manage' ? (
          // Edit mode: seed the email from the URL if present, else this browser's remembered
          // address. OnboardingPage fetches /preferences for it and prefills; if there is no
          // email it shows the email step first (ask once), then prefills.
          <OnboardingPage seedEmail={seedEmail || rememberedEmail()} manageMode={true} onNav={go} />
        ) : page5 === 'newsletters' ? (
          <NewslettersRoute onNav={go} />
        ) : page5 === 'privacy' ? (
          <LegalPage doc="privacy" onNav={go} />
        ) : page5 === 'terms' ? (
          <LegalPage doc="terms" onNav={go} />
        ) : (
          <TeamPage />
        )}
        {page5 === 'onboarding' || page5 === 'manage' ? null : <SiteFooter onNav={go} page={page5} />}
      </div>
    </div>
  );
}
// ---- email image-export mode ----------------------------------------------
// When loaded as ?card_export=1&team=IND&date=2025-12-08 the page renders ONLY the
// newsletter scaffolding for that (team,date), with per-card data-card anchors and NO
// nav/selector/footer chrome — a clean surface for the email image-render pipeline
// (scripts/render_email_from_site.py) to screenshot each card. This path is inert for
// normal visitors (guarded by the query param) so it never affects the live site.
function ExportScaffold({ dataError }) {
  const q = new URLSearchParams(location.search);
  const date = q.get('date');
  // LEAGUE mode: ?card_export=1&league=1&date=YYYY-MM-DD renders the teamless "All 32" edition
  // (window.SYNTH_LEAGUE_EDITIONS[date]) via SynthLeagueEdition; TEAM mode keys by team_date.
  const isLeague = q.get('league') === '1';  // exact contract the league renderer passes (?league=1)
  const team = q.get('team');
  const key = isLeague ? date : (team + '_' + date);
  const issue = isLeague
    ? (window.SYNTH_LEAGUE_EDITIONS || {})[date]
    : (window.SYNTH_ISSUES || {})[key];
  // A failed data load is reported as such, not as a missing issue, so the renderer's error points
  // at the asset rather than at the scaffolding row.
  const errCode = dataError ? 'data-load-failed' : 'no-issue';
  React.useEffect(() => {
    // signal readiness for the renderer to wait on (after fonts settle it screenshots)
    if (issue) document.documentElement.setAttribute('data-export-ready', '1');
    else document.documentElement.setAttribute('data-export-error', errCode);
  }, []);
  return (
    <div style={{ background:'var(--syn-paper, #FBF7F1)', padding:'0' }}>
      {issue
        ? (isLeague
            ? <window.SynthLeagueEdition issue={issue} exportMode={true} />
            : <window.SynthScaffolding issue={issue} exportMode={true} />)
        : <div data-export-error={errCode}>{dataError ? 'Newsletter data failed to load' : `No issue for ${key}`}</div>}
    </div>
  );
}

(function mount() {
  const q = new URLSearchParams(location.search);
  const root = ReactDOM.createRoot(document.getElementById('root'));
  // ExportScaffold decides ready/no-issue ONCE, on its first render, so the data must exist before
  // it mounts. Under export.html it already does (synchronous tag) and this resolves immediately;
  // on a failed load it still mounts and reports data-export-error="data-load-failed", so the
  // renderer fails loudly.
  if (q.get('card_export')) {
    loadNewsletterData().then(() => root.render(<ExportScaffold />),
                              () => root.render(<ExportScaffold dataError={true} />));
  }
  else root.render(<Site />);
})();
