// ─────────────────────────────────────────────────────────────
// LifeWheel Coach — claim invite screen.
// Reached via the Hosting rewrite at /coach-invite/{token} OR
// directly at /coach/Claim.html?token={token}.
//
// Flow:
//   1. Extract token from URL (path tail OR ?token=).
//   2. Read /coach_invites/{token} (rules: public read).
//   3. If no auth → route to Login with the full claim URL preserved as ?next=.
//   4. If invite is expired/cancelled/claimed → render explainer state.
//   5. Show coach the invite info + scope checklist (sourced from defaultScopes).
//   6. On confirm: atomic multi-path update at:
//        /users/{userUid}/coach_link
//        /coach_clients/{coachId}/{userUid}
//        /coach_invites/{token}/{status,coachId,coachName,claimedAt}
//   7. Redirect to Dashboard.
// ─────────────────────────────────────────────────────────────

const SCOPE_META = [
  { key: 'wheel',   labelKey: 'claim.scope_wheel',   hintKey: 'claim.scope_wheel_hint' },
  { key: 'habits',  labelKey: 'claim.scope_habits',  hintKey: 'claim.scope_habits_hint' },
  { key: 'tasks',   labelKey: 'claim.scope_tasks',   hintKey: 'claim.scope_tasks_hint' },
  { key: 'mood',    labelKey: 'claim.scope_mood',    hintKey: 'claim.scope_mood_hint' },
  { key: 'journal', labelKey: 'claim.scope_journal', hintKey: 'claim.scope_journal_hint', sensitive: true },
  { key: 'who5',    labelKey: 'claim.scope_who5',    hintKey: 'claim.scope_who5_hint',    sensitive: true },
];

function parseTokenFromURL() {
  // /coach-invite/{token}  → path segment after "coach-invite"
  // /coach/Claim.html?token={token} → query param
  // /coach/app.html#/claim/{token} → SPA route
  if (window.LWRouter) {
    const r = window.LWRouter.parseRoute();
    if (r.name === 'claim' && r.params && r.params.token) return r.params.token;
  }
  const path = window.location.pathname;
  const match = path.match(/\/coach-invite\/([^/?#]+)/);
  if (match && match[1]) return decodeURIComponent(match[1]);
  const sp = new URLSearchParams(window.location.search);
  const t = sp.get('token');
  return t ? t.trim() : null;
}

function LWClaim({ routeParams }) {
  const { c: cBase, fonts: fontsBase, t } = useLW();
  const { isMobile } = useViewport();
  // Brutalism theme override — same cream/dark toggle as signup + login
  const [brutMode, setBrutMode] = window.LWBrutal.useBrutMode();
  const c = window.LWBrutal.makeC(cBase, brutMode);
  const fonts = { ...fontsBase, ...window.LWBrutal.fonts };
  const [token] = useState(() => (routeParams && routeParams.token) || parseTokenFromURL());
  // `?from=coach` marks coach-initiated invites — those are for the *client*
  // to accept on iOS, not for a coach to claim from web. We render an
  // explainer instead of the claim UI so a coach who tapped their own link
  // sees clear guidance rather than a doomed write attempt.
  const isCoachInitiatedHint = (() => {
    try { return new URLSearchParams(window.location.search).get('from') === 'coach'; }
    catch { return false; }
  })();
  const [authedCoach, setAuthedCoach] = useState(null);
  const [authResolved, setAuthResolved] = useState(false);
  const [invite, setInvite] = useState(null);          // raw object from /coach_invites/{token}
  const [inviteState, setInviteState] = useState('loading'); // loading|missing|expired|cancelled|claimed|ready|notfound
  const [scopes, setScopes] = useState({ wheel: true, habits: true, tasks: true, mood: true, journal: false, who5: false });
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  // Auth listener — preserve original URL via ?next= when redirecting to Login.
  useEffect(() => {
    if (!window.LWAuth) return;
    return window.LWAuth.onAuthChanged((u) => {
      setAuthedCoach(u);
      setAuthResolved(true);
    });
  }, []);

  // Load invite once.
  useEffect(() => {
    if (!token) { setInviteState('missing'); return; }
    if (!window.LWFB) return;
    window.LWFB.db.ref(`/coach_invites/${token}`).get()
      .then((snap) => {
        if (!snap.exists()) { setInviteState('notfound'); return; }
        const v = snap.val();
        setInvite(v);
        // Coach-initiated tokens (`invitedBy: 'coach'`) belong to the iOS
        // accept flow — surface an explainer here rather than walking the
        // client-initiated claim path with a missing userUid.
        if (v.invitedBy === 'coach' || isCoachInitiatedHint) {
          setInviteState('coachInitiated');
          return;
        }
        setScopes({
          wheel:   v.defaultScopes?.wheel   ?? true,
          habits:  v.defaultScopes?.habits  ?? true,
          tasks:   v.defaultScopes?.tasks   ?? true,
          mood:    v.defaultScopes?.mood    ?? true,
          journal: v.defaultScopes?.journal ?? false,
          who5:    v.defaultScopes?.who5    ?? false,
        });
        const now = Date.now();
        if (v.status === 'cancelled') { setInviteState('cancelled'); return; }
        if (v.status === 'claimed')   { setInviteState('claimed');   return; }
        if (v.expiresAt && now > Number(v.expiresAt)) { setInviteState('expired'); return; }
        setInviteState('ready');
      })
      .catch((err) => {
        setError('Could not read this invite — ' + (err.message || err));
        setInviteState('notfound');
      });
  }, [token]);

  // We no longer auto-redirect unauth visitors to the login form. The
  // invite preview is public-readable and showing the coach what's being
  // shared BEFORE asking them to sign in is more honest. The Accept button
  // routes unauth users to Signup with `?next=<this URL>`; after signup
  // they bounce back here authed and the claim runs automatically.

  const claim = async () => {
    if (!authedCoach || !invite || !token) return;
    // Coach-initiated invites have no userUid yet — those are claimed from the
    // iOS app, not the web. Belt-and-suspenders against UI getting into a
    // 'ready' state when the parent guard misses the invitedBy flag.
    if (!invite.userUid) {
      setError("This link is for your client to accept from the LifeWheel iOS app.");
      return;
    }
    setSubmitting(true); setError(null);
    try {
      const coachId = authedCoach.uid;
      const coachName = (authedCoach.profile && authedCoach.profile.displayName) || authedCoach.email || 'Coach';
      const coachInitials = (authedCoach.profile && authedCoach.profile.initials)
        || (window.LWAuth ? window.LWAuth.initialsOf(coachName) : '·');
      const userUid = invite.userUid;
      const now = Date.now();
      const updates = {
        // Owner-side singleton — created on first claim. Owner mutates scopes after.
        [`/users/${userUid}/coach_link`]: {
          token,
          coachId,
          coachName,
          claimedAt: now,
          status: 'active',
          scopes,
        },
        // Coach-side roster index. Display data the dashboard reads at-a-glance.
        [`/coach_clients/${coachId}/${userUid}`]: {
          status: 'active',
          connectedAt: now,
          token,
          // Coach-side mirror — let coach edit their nickname for the client later.
          // Until then, fall back to "New client" since iOS users may be anonymous.
          displayName: invite.userName || invite.clientName || 'New client',
          coachName,
          coachInitials,
        },
        // Mark invite claimed for owner-side observation + audit trail.
        [`/coach_invites/${token}/status`]:    'claimed',
        [`/coach_invites/${token}/coachId`]:   coachId,
        [`/coach_invites/${token}/coachName`]: coachName,
        [`/coach_invites/${token}/claimedAt`]: now,
      };
      await window.LWFB.db.ref('/').update(updates);
      setSuccess(true);
      // Land on the new client's page so the coach sees what they just
      // connected to, not a generic dashboard.
      const clientHref = '#/client/' + encodeURIComponent(userUid);
      setTimeout(() => {
        window.location.replace(window.location.pathname + window.location.search + clientHref);
      }, 1200);
    } catch (err) {
      setError(prettyClaimError(err));
      setSubmitting(false);
    }
  };

  // ── render variants ───────────────────────────────────────────
  // Coach-initiated invites are for clients on iOS — render a no-auth
  // landing page that deeplinks into the LifeWheel iOS app (or sends them
  // to the App Store if not installed).
  if (inviteState === 'coachInitiated') {
    return <CoachInitiatedLanding c={c} fonts={fonts} token={token} invite={invite} />;
  }
  // Show invite-loading shell to everyone while we fetch — no auth check
  // needed because the doc is public-readable.
  if (inviteState === 'loading') {
    return <ClaimShell c={c} fonts={fonts} brutMode={brutMode} setBrutMode={setBrutMode}><Loading c={c} fonts={fonts} label={t('claim.loading')} /></ClaimShell>;
  }
  if (inviteState === 'missing') {
    return <ClaimShell c={c} fonts={fonts} brutMode={brutMode} setBrutMode={setBrutMode}><Explainer c={c} fonts={fonts}
      title="No invite token in the link."
      body="The link your client sent should look like https://lifewheel.app/coach-invite/… — ask them to share it again."
      cta="Open dashboard" href="/coach/Dashboard.html" /></ClaimShell>;
  }
  if (inviteState === 'notfound') {
    return <ClaimShell c={c} fonts={fonts} brutMode={brutMode} setBrutMode={setBrutMode}><Explainer c={c} fonts={fonts}
      title="That invite is not in our system."
      body={error || "It may have been cancelled or never reached us. Ask your client to generate a fresh link."}
      cta="Open dashboard" href="/coach/Dashboard.html" /></ClaimShell>;
  }
  if (inviteState === 'cancelled') {
    return <ClaimShell c={c} fonts={fonts} brutMode={brutMode} setBrutMode={setBrutMode}><Explainer c={c} fonts={fonts}
      title="This invite was cancelled."
      body="Your client cancelled the link before you could claim it. Ask them to generate a new one if they still want to connect."
      cta="Open dashboard" href="/coach/Dashboard.html" /></ClaimShell>;
  }
  if (inviteState === 'expired') {
    return <ClaimShell c={c} fonts={fonts} brutMode={brutMode} setBrutMode={setBrutMode}><Explainer c={c} fonts={fonts}
      title="This invite expired."
      body="Invites are valid for 30 days. Ask your client to generate a fresh link."
      cta="Open dashboard" href="/coach/Dashboard.html" /></ClaimShell>;
  }
  if (inviteState === 'claimed') {
    const yours = invite.coachId === authedCoach.uid;
    return <ClaimShell c={c} fonts={fonts} brutMode={brutMode} setBrutMode={setBrutMode}><Explainer c={c} fonts={fonts}
      title={yours ? t('claim.claimed_yours_title') : t('claim.claimed_other_title')}
      body={yours ? t('claim.claimed_yours_body') : t('claim.claimed_other_body')}
      cta={t('claim.cta_open_dashboard')} href="/coach/Dashboard.html" /></ClaimShell>;
  }

  // inviteState === 'ready' — show the claim UI
  return (
    <ClaimShell c={c} fonts={fonts} brutMode={brutMode} setBrutMode={setBrutMode}>
      <div className="lwb-reveal" style={{
        width: '100%', maxWidth: 540,
        background: c.card, border: `1.5px solid ${c.ink}`,
        borderRadius: 2, padding: isMobile ? 28 : 36,
        boxShadow: `6px 6px 0 0 ${c.ink}`,
      }}>
        <div style={{
          fontFamily: fonts.body, fontSize: 11, fontWeight: 700, letterSpacing: '0.16em',
          textTransform: 'uppercase', color: c.accent, marginBottom: 12,
        }}>{t('claim.eyebrow')}</div>

        <h1 style={{
          fontFamily: fonts.display, fontSize: isMobile ? 28 : 34, fontWeight: 600, lineHeight: 1.1,
          letterSpacing: '-0.015em', color: c.textPrimary, margin: 0, marginBottom: 10,
        }}>
          {t('claim.title_pre')} <em style={{ fontStyle: 'italic', color: c.accent }}>{t('claim.title_em')}</em>
        </h1>
        <p style={{
          fontFamily: fonts.display, fontStyle: 'italic', fontSize: 15,
          color: c.textSecondary, margin: 0, marginBottom: 22,
        }}>
          {t('claim.subtitle')}
        </p>

        <InviteFacts c={c} fonts={fonts} invite={invite} />

        <div style={{ marginTop: 18, marginBottom: 8 }}>
          <div style={{
            fontFamily: fonts.body, fontSize: 11, fontWeight: 700, letterSpacing: '0.14em',
            textTransform: 'uppercase', color: c.textTertiary, marginBottom: 6,
          }}>{t('claim.scopes_title')}</div>
          <div style={{ fontFamily: fonts.body, fontSize: 13, color: c.textTertiary, marginBottom: 14, fontStyle: 'italic' }}>
            {t('claim.scopes_hint')}
          </div>

          <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'grid', gap: 8 }}>
            {SCOPE_META.map(s => (
              <li key={s.key} style={{
                display: 'flex', alignItems: 'flex-start', gap: 12,
                padding: '12px 14px', borderRadius: 12,
                background: scopes[s.key] ? c.accentMuted : c.bgSubtle,
                border: `1px solid ${scopes[s.key] ? c.borderDefault : c.borderSubtle}`,
              }}>
                <span style={{
                  flexShrink: 0, width: 22, height: 22, borderRadius: 6,
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  background: scopes[s.key] ? c.accent : 'transparent',
                  border: scopes[s.key] ? `1px solid ${c.accent}` : `1px solid ${c.borderDefault}`,
                  color: c.textOnAccent, fontFamily: fonts.app, fontWeight: 700, fontSize: 14, marginTop: 1,
                }}>{scopes[s.key] ? '✓' : ''}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{
                    fontFamily: fonts.body, fontSize: 14, fontWeight: 600,
                    color: scopes[s.key] ? c.textPrimary : c.textSecondary,
                  }}>
                    {t(s.labelKey)}
                    {s.sensitive && <span style={{ marginLeft: 8, fontSize: 10, fontWeight: 700, letterSpacing: '0.10em', color: c.flame, textTransform: 'uppercase' }}>{t('claim.sensitive')}</span>}
                  </div>
                  <div style={{ fontFamily: fonts.body, fontSize: 12, color: c.textTertiary, lineHeight: 1.5 }}>{t(s.hintKey)}</div>
                </div>
              </li>
            ))}
          </ul>
        </div>

        {error && (
          <div style={{
            marginTop: 16, padding: '10px 14px', borderRadius: 10,
            background: hexToRgba(c.error, 0.10),
            border: `1px solid ${hexToRgba(c.error, 0.3)}`,
            color: c.textPrimary, fontFamily: fonts.body, fontSize: 13,
          }}>{error}</div>
        )}

        {success ? (
          <div style={{
            marginTop: 22, padding: '14px 18px', borderRadius: 12,
            background: c.accentMuted, border: `1px solid ${c.borderDefault}`,
            color: c.textPrimary, fontFamily: fonts.body, fontSize: 14,
            textAlign: 'center',
          }}>
            ✓ {t('claim.connected')}
          </div>
        ) : !authResolved ? (
          // Auth still resolving — render a non-pushy placeholder for the
          // CTA so the layout doesn't jump once auth lands.
          <div style={{ marginTop: 22 }}>
            <Loading c={c} fonts={fonts} label={t('claim.loading')} />
          </div>
        ) : !authedCoach ? (
          // Unauthed: route Accept to Signup with this URL as `?next=`. After
          // signup we bounce back here with auth resolved → claim() runs from
          // the existing render path.
          <div style={{ marginTop: 22, display: 'grid', gap: 10 }}>
            <button onClick={() => {
              const here = '#/claim/' + encodeURIComponent(token || '');
              const url = '/coach/Signup.html?next=' + encodeURIComponent(here);
              window.location.href = url;
            }} style={{
              padding: '14px 22px', borderRadius: 12, border: 'none', cursor: 'pointer',
              background: c.accent, color: c.textOnAccent,
              fontFamily: fonts.app, fontSize: 15, fontWeight: 700, letterSpacing: '0.01em',
            }}>
              {t('claim.accept_signup_cta')}
            </button>
            <a href={'/coach/Login.html?next=' + encodeURIComponent('#/claim/' + encodeURIComponent(token || ''))} style={{
              padding: '12px 22px', borderRadius: 12,
              fontFamily: fonts.body, fontSize: 14, color: c.textSecondary,
              textDecoration: 'none', textAlign: 'center',
              border: `1px solid ${c.borderDefault}`,
            }}>{t('claim.have_account_signin')}</a>
          </div>
        ) : (
          <div style={{ marginTop: 22, display: 'grid', gap: 10 }}>
            <button onClick={claim} disabled={submitting} style={{
              padding: '14px 22px', borderRadius: 12, border: 'none',
              cursor: submitting ? 'progress' : 'pointer',
              background: submitting ? c.accentMuted : c.accent,
              color: c.textOnAccent,
              fontFamily: fonts.app, fontSize: 15, fontWeight: 700, letterSpacing: '0.01em',
              opacity: submitting ? 0.7 : 1, transition: 'opacity 150ms ease',
            }}>
              {submitting ? t('claim.connecting') : t('claim.accept_cta')}
            </button>
            <a href="/coach/Dashboard.html" style={{
              padding: '12px 22px', borderRadius: 12,
              fontFamily: fonts.body, fontSize: 14, color: c.textSecondary,
              textDecoration: 'none', textAlign: 'center',
              border: `1px solid ${c.borderDefault}`,
            }}>{t('claim.not_now')}</a>
          </div>
        )}

        {authedCoach && (
          <div style={{
            marginTop: 22, paddingTop: 18,
            borderTop: `1px solid ${c.borderSubtle}`,
            fontFamily: fonts.body, fontSize: 12, color: c.textTertiary, lineHeight: 1.5,
          }}>
            {t('claim.signed_in_as')} <strong style={{ color: c.textSecondary }}>{authedCoach.email}</strong>. <button onClick={() => window.LWAuth.signOut().then(() => window.location.reload())} style={{
              background: 'none', border: 'none', cursor: 'pointer', padding: 0,
              color: c.textSecondary, textDecoration: 'underline', font: 'inherit',
            }}>{t('claim.switch_account')}</button>
          </div>
        )}
      </div>
    </ClaimShell>
  );
}

// Client-facing landing for coach-initiated invites. Designed for a phone
// browser — auto-deeplinks into the LifeWheel iOS app, falls back to the
// App Store, and offers a manual "Open in app" button if the auto-launch
// gets blocked by the browser's user-gesture policy.
const APP_STORE_URL = 'https://apps.apple.com/app/apple-store/id988402523?pt=117755042&ct=coach-invite&mt=8';

function CoachInitiatedLanding({ c, fonts, token, invite }) {
  const t = window.LWLang ? window.LWLang.t : (k) => k;
  const lang = window.LWLang ? window.LWLang.lang() : 'en';
  const deeplink = `LifeWheel://coach-invite/${encodeURIComponent(token || '')}`;
  const ua = (navigator.userAgent || '').toLowerCase();
  const isIOS = /iphone|ipad|ipod/.test(ua);
  const coachName = (invite && invite.coachName) || '';

  // One-shot auto-deeplink attempt on iOS. If the app is installed, the OS
  // intercepts the navigation and switches to LifeWheel; if not, the
  // navigation 404s silently and the page stays put. We intentionally do
  // NOT auto-redirect to the App Store on failure — Safari treats unhandled
  // schemes as user-cancellation, and chained redirects feel hostile.
  React.useEffect(() => {
    if (!isIOS || !token) return;
    const tid = setTimeout(() => {
      try { window.location.href = deeplink; } catch {}
    }, 200);
    return () => clearTimeout(tid);
  }, [isIOS, token, deeplink]);

  return (
    <ClaimShell c={c} fonts={fonts} brutMode={brutMode} setBrutMode={setBrutMode}>
      <div style={{
        width: '100%', maxWidth: 460,
        background: c.card, border: `1px solid ${c.borderSubtle}`,
        borderRadius: 22, padding: 32,
        boxShadow: '0 24px 60px rgba(0,0,0,0.30)',
      }}>
        <div style={{
          fontFamily: fonts.body, fontSize: 11, fontWeight: 700, letterSpacing: '0.16em',
          textTransform: 'uppercase', color: c.accent, marginBottom: 12,
        }}>{t('coach_landing.eyebrow') === 'coach_landing.eyebrow' ? (lang === 'ru' ? 'Приглашение от коуча' : 'Coach invite') : t('coach_landing.eyebrow')}</div>

        <h1 style={{
          fontFamily: fonts.display, fontSize: 28, fontWeight: 600, lineHeight: 1.15,
          letterSpacing: '-0.015em', color: c.textPrimary, margin: 0, marginBottom: 12,
        }}>
          {coachName
            ? (lang === 'ru' ? <>{coachName} <em style={{ fontStyle: 'italic', color: c.accent }}>хочет работать с тобой.</em></>
                              : <>{coachName} <em style={{ fontStyle: 'italic', color: c.accent }}>wants to coach you.</em></>)
            : (lang === 'ru' ? <>Твой коуч <em style={{ fontStyle: 'italic', color: c.accent }}>пригласил тебя.</em></>
                              : <>Your coach <em style={{ fontStyle: 'italic', color: c.accent }}>invited you.</em></>)}
        </h1>

        <p style={{
          fontFamily: fonts.display, fontStyle: 'italic', fontSize: 15,
          color: c.textSecondary, margin: 0, marginBottom: 22, lineHeight: 1.5,
        }}>
          {lang === 'ru'
            ? 'Открой LifeWheel на iPhone и подтверди связь. Вы решите, чем делиться — это можно изменить в любой момент.'
            : "Open LifeWheel on your iPhone to accept. You'll decide what to share — and you can change it anytime."}
        </p>

        <a href={deeplink} style={{
          display: 'block', padding: '14px 22px', borderRadius: 12,
          background: c.accent, color: c.textOnAccent,
          textAlign: 'center', textDecoration: 'none',
          fontFamily: fonts.app, fontSize: 15, fontWeight: 700, letterSpacing: '0.01em',
          marginBottom: 10,
        }}>
          {lang === 'ru' ? 'Открыть в LifeWheel' : 'Open in LifeWheel'}
        </a>

        <a href={APP_STORE_URL} style={{
          display: 'block', padding: '12px 22px', borderRadius: 12,
          background: 'transparent', border: `1px solid ${c.borderDefault}`,
          color: c.textSecondary,
          textAlign: 'center', textDecoration: 'none',
          fontFamily: fonts.body, fontSize: 14,
        }}>
          {lang === 'ru' ? "Нет приложения? Установить из App Store" : "Don't have the app? Get it on the App Store"}
        </a>

        {!isIOS && (
          <div style={{
            marginTop: 18, padding: '12px 14px', borderRadius: 10,
            background: c.bgSubtle, border: `1px solid ${c.borderSubtle}`,
            fontFamily: fonts.body, fontSize: 12, color: c.textTertiary, lineHeight: 1.5,
          }}>
            {lang === 'ru'
              ? 'LifeWheel есть только на iPhone. Открой эту ссылку на iPhone, чтобы принять приглашение.'
              : 'LifeWheel is iPhone-only. Open this link on your iPhone to accept the invite.'}
          </div>
        )}
      </div>
    </ClaimShell>
  );
}

function ClaimShell({ c, fonts, brutMode, setBrutMode, children }) {
  const Atmosphere = window.LWBrutal && window.LWBrutal.Atmosphere;
  const Styles = window.LWBrutal && window.LWBrutal.Styles;
  const ModeToggle = window.LWBrutal && window.LWBrutal.ModeToggle;
  return (
    <div style={{
      minHeight: '100vh', position: 'relative', overflow: 'hidden',
      background: c.bg, color: c.textPrimary,
      fontFamily: fonts.mono,
    }}>
      {Atmosphere && <Atmosphere c={c} brutMode={brutMode} />}
      <div style={{ position: 'relative', zIndex: 1, minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
        {/* Top strip — mark + toggles */}
        <div style={{
          padding: '14px 24px',
          borderBottom: `1.5px solid ${c.ink}`,
          display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16,
        }}>
          <a href="#/dashboard" style={{ display: 'flex', alignItems: 'center', gap: 10, textDecoration: 'none' }}>
            <ClaimMark c={c} size={26} />
            <span style={{ fontFamily: fonts.mono, fontSize: 14, fontWeight: 700, color: c.ink, letterSpacing: '0.04em', textTransform: 'uppercase' }}>
              LifeWheel<span style={{ color: c.textTertiary, marginLeft: 6 }}>.coach</span>
            </span>
          </a>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            {ModeToggle && setBrutMode && <ModeToggle c={c} fonts={fonts} brutMode={brutMode} setBrutMode={setBrutMode} />}
            {window.LangToggle && <window.LangToggle c={c} fonts={fonts} />}
          </div>
        </div>
        <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px 20px' }}>
          {children}
        </div>
      </div>
      {Styles && <Styles c={c} fonts={fonts} />}
    </div>
  );
}

function ClaimMark({ c, size = 28 }) {
  // Defensive: if the theme provider hasn't published spheres yet (rare
  // during error-recovery renders, or if useLW falls back to a stub),
  // render a single-color circle rather than crashing the whole page.
  const sphereColors = (c && c.spheres) ? Object.values(c.spheres) : [(c && c.accent) || '#3EC08D'];
  const r = size / 2;
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
      {sphereColors.map((col, i) => {
        const a0 = (i / 8) * Math.PI * 2 - Math.PI / 2;
        const a1 = ((i + 1) / 8) * Math.PI * 2 - Math.PI / 2;
        const x0 = r + Math.cos(a0) * r, y0 = r + Math.sin(a0) * r;
        const x1 = r + Math.cos(a1) * r, y1 = r + Math.sin(a1) * r;
        return <path key={i} d={`M${r} ${r} L${x0} ${y0} A${r} ${r} 0 0 1 ${x1} ${y1} Z`} fill={col} opacity={0.85} />;
      })}
      <circle cx={r} cy={r} r={r * 0.32} fill={c.bg} />
    </svg>
  );
}

function Loading({ c, fonts, label }) {
  return (
    <div style={{
      maxWidth: 460, padding: 36, textAlign: 'center', borderRadius: 16,
      background: c.card, border: `1px solid ${c.borderSubtle}`,
      fontFamily: fonts.display, fontStyle: 'italic', fontSize: 16, color: c.textSecondary,
    }}>
      {label}
    </div>
  );
}

function Explainer({ c, fonts, title, body, cta, href }) {
  return (
    <div style={{
      width: '100%', maxWidth: 460,
      background: c.card, border: `1px solid ${c.borderSubtle}`,
      borderRadius: 20, padding: 36, boxShadow: '0 24px 60px rgba(0,0,0,0.30)',
    }}>
      <h1 style={{
        fontFamily: fonts.display, fontSize: 26, fontWeight: 600, lineHeight: 1.15,
        letterSpacing: '-0.01em', color: c.textPrimary, margin: 0, marginBottom: 10,
      }}>{title}</h1>
      <p style={{
        fontFamily: fonts.display, fontStyle: 'italic', fontSize: 15,
        color: c.textSecondary, margin: 0, marginBottom: 22,
      }}>{body}</p>
      <a href={href} style={{
        display: 'inline-block', padding: '12px 22px', borderRadius: 12,
        background: c.accent, color: c.textOnAccent, textDecoration: 'none',
        fontFamily: fonts.app, fontSize: 14, fontWeight: 700, letterSpacing: '0.01em',
      }}>{cta}</a>
    </div>
  );
}

function InviteFacts({ c, fonts, invite }) {
  const t = window.LWLang ? window.LWLang.t : (k) => k;
  const lang = window.LWLang ? window.LWLang.lang() : 'en';
  const loc = lang === 'ru' ? 'ru-RU' : undefined;
  const created = invite.createdAt ? new Date(Number(invite.createdAt)) : null;
  const expires = invite.expiresAt ? new Date(Number(invite.expiresAt)) : null;
  const fmt = (d) => d ? d.toLocaleDateString(loc, { month: 'short', day: 'numeric', year: 'numeric' }) : '—';
  const days = expires ? Math.max(0, Math.ceil((expires.getTime() - Date.now()) / (1000 * 60 * 60 * 24))) : null;
  const daysKey = (n) => {
    if (lang !== 'ru') return n === 1 ? 'claim.days_left' : 'claim.days_left_pl';
    const m10 = n % 10, m100 = n % 100;
    if (m10 === 1 && m100 !== 11) return 'claim.days_left';
    if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return 'claim.days_left_few';
    return 'claim.days_left_pl';
  };
  return (
    <div style={{
      padding: '14px 16px', borderRadius: 12,
      background: c.bgSubtle, border: `1px solid ${c.borderSubtle}`,
      display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14,
      fontFamily: fonts.body, fontSize: 12, color: c.textTertiary,
    }}>
      <div>
        <div style={{ fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', marginBottom: 4 }}>{t('claim.created_label')}</div>
        <div style={{ color: c.textPrimary, fontSize: 14 }}>{fmt(created)}</div>
      </div>
      <div>
        <div style={{ fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', marginBottom: 4 }}>{t('claim.valid_label')}</div>
        <div style={{ color: c.textPrimary, fontSize: 14 }}>
          {days != null ? t(daysKey(days), { n: days }) : '—'}
        </div>
      </div>
    </div>
  );
}

function prettyClaimError(err) {
  const code = err && err.code ? err.code : '';
  if (typeof err === 'object' && err && /permission/i.test(err.message || '')) {
    return "We couldn't claim this invite — the rules denied the write. The token may have been claimed in the last few seconds. Try opening the dashboard.";
  }
  switch (code) {
    case 'PERMISSION_DENIED': return "Can't claim this invite — likely already claimed. Try opening your dashboard.";
    case 'NETWORK_ERROR':     return 'Network hiccup — check your connection and try again.';
    default: return (err && err.message) || 'Something went wrong. Try again.';
  }
}
