/* ═══════════════════════════════════════════════════════════════════
   SeoSurface.jsx — SEO Tracker surface controller (overview ↔ detail ↔
   empty), LIVE data. v2.0.0 — real-data SWAP of the v2 Increment-2 shell.
   v1 ran the SeoData.jsx sample set, SA-only. v2 wires the live, account-
   scoped /seo/rank/* endpoints via window.WPSB.SeoLive.useSeoLiveData()
   and renders honest loading / error / locked-upgrade / empty / ready
   states. NO API key in the browser — JWT/session only (window.WPSB.getToken).

   Honest states (Radical Transparency, locked rule #13):
     · loading  → skeleton, never a fake zero
     · error    → "Couldn't load — check your connection" + Retry (no silent blank)
     · locked   → free tier: feature de-emphasized + upgrade CTA (never hidden)
     · empty    → first-run add-keyword
     · ready    → live table + freshness/coverage banner (as-of + what's pending)
   Columns the rank endpoints don't return (search volume, difficulty, AI
   visibility, competitors, suggestions, traffic) render '—'/empty, never 0;
   each is an api-lane gap (see PR).

   ── ACCESS DECLARATION (default-deny; _ROLE-PERMISSION-COMPLETENESS §0) ──
   Full role-completeness unlock (every dimension; NAV_<ROLE> in Shell.jsx +
   ALLOWED.<role> in App.jsx wired for EACH — missing either breaks the tab):
     super_admin/sa  ✓ (nav + allowed; View-As covers every persona view)
     customer        ✓ (+ internal_partner via the customer inherit)
     marketing_admin ✓ (+ marketing_jr inherit)
     dev_admin       ✓ (→ 'sa' persona) (+ dev_jr inherit)
     admin           ✓
     support_agent   ✓        support_admin ✓
   AGENCY ROLE: owner / dev / member may view + track via the customer
   agency-role gate. STAFF = view/operate ONLY — this surface performs NO
   entitlement mutation; changing a customer's plan/allowance is the admin
   four-verb 'Re-price' line and stays SA-only. Staff act in their own account
   scope; cross-account view is SA-only via View-As impersonation.
   PLAN GATE = the live allowance: keyword_allowance 0 ⇒ locked-upgrade (this
   renders for a real under-tier customer, not just SA impersonation, because
   it keys off the live /seo/rank/targets allowance); Solo+ ⇒ weekly tracking;
   daily = Starter+ SEO add-on ($39/$79/$149, §6 #22) gated off server-side
   (402 SEO_DAILY_UPGRADE_REQUIRED).
   Nav visibility never widens API scope — endpoints self-gate per account.

   ── ANALYTICS (locked rule #32) ─────────────────────────────────────
   Server-side trackEvent already fires on the real actions: seo.keyword.tracked
   (POST /seo/rank/targets) + seo.rank.checked (POST /seo/rank/run-now). This
   surface ALSO emits two CLIENT view events to the customer-scoped ingest
   POST /client/events/track (requireJWT('wpsb-app'); account_id+user_id are
   taken FROM the JWT, never the body; unknown keys dropped; 202 fire-and-forget,
   never blocks the UI). Keys reserved server-side (api #82 / v1.85.0.0.0):
     · seo.rank.viewed     — overview opened/loaded (fired once on mount)
     · seo.keyword.opened  — a keyword/target detail opened
   NOTE: the SA-only POST /events/track (trusts body account_id) is NOT used
   here — emitting there would silently drop for a customer JWT.

   Load order: SIXTH (after SeoCharts/SeoData/SeoLive/SeoTracker/SeoDetail).
   Registers: window.SeoSurface (App.jsx router target) + window.WPSB.SeoSurface.
   ═══════════════════════════════════════════════════════════════════ */
(function () {
const { useState: useStateS2, useEffect: useEffectS2, useCallback: useCbS2 } = React;
const WIcon = window.WIcon;

function apiBase() { return (window.WPSBD && window.WPSBD.apiBase) || 'https://api.wpsitebeam.io'; }
function authToken() {
  return (window.WPSB && window.WPSB.getToken && window.WPSB.getToken())
      || (function () { try { return localStorage.getItem('wpsb-auth-token'); } catch (e) { return null; } })();
}
function toast(text, kind) { if (window.wpsbToast) window.wpsbToast(text, kind || 'beam'); }

/* Customer-scoped client analytics ingest. account_id + user_id come from the
   JWT server-side — we NEVER send account_id in the body. Fire-and-forget:
   any failure is swallowed so analytics can never block or break the surface.
   Targets /client/events/track (NOT the SA-only /events/track, which would
   silently drop a customer JWT's event). */
function emitClientEvent(eventType, props) {
  try {
    const token = authToken(); if (!token) return;
    fetch(apiBase() + '/client/events/track', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
      body: JSON.stringify({ event_type: eventType, props: props || {} }),
    }).catch(function () {});
  } catch (_) { /* swallow — analytics never blocks the surface */ }
}

/* normalize an input domain the way the API does (host only) */
function cleanDomain(d) {
  return String(d || '').toLowerCase().replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/[\/?#].*$/, '').trim();
}

/* ── small primitives ──────────────────────────────────────────────── */
function Card(children, extra) {
  return React.createElement('div', { className: 'wiz-card', style: Object.assign({ background: 'var(--surface)',
    border: '1px solid var(--border)', borderRadius: 'var(--r-xl)', padding: 'var(--sp-6)' }, extra || {}) }, children);
}
function Header() {
  return React.createElement('div', null,
    React.createElement('div', { className: 'wiz-eyebrow', style: { marginBottom: 6 } }, 'SEO · Rank tracking'),
    React.createElement('h1', { className: 'wiz-title', style: { margin: 0, fontSize: '1.6rem' } }, 'SEO Tracker'));
}

/* loading skeleton */
function LoadingState() {
  return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 'var(--sp-5)' } }, Header(),
    Card(React.createElement('div', { role: 'status', 'aria-live': 'polite', style: { display: 'flex', alignItems: 'center', gap: 12, color: 'var(--text-2)' } },
      React.createElement('span', { className: 'wiz-spin', style: { width: 18, height: 18, borderRadius: '50%', border: '2px solid var(--border-2)', borderTopColor: 'var(--beam)', display: 'inline-block', animation: 'wpsbdspin 0.8s linear infinite' } }),
      React.createElement('span', { style: { fontFamily: 'var(--font-mono)', fontSize: '.85rem' } }, 'Loading your live rank data…'))));
}

/* honest error — never a silent blank or a fake zero */
function ErrorState({ message, onRetry }) {
  return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 'var(--sp-5)' } }, Header(),
    Card([
      React.createElement('div', { key: 'i', style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 } },
        React.createElement('span', { style: { color: 'var(--warn)', display: 'inline-flex' } }, React.createElement(WIcon, { name: 'alert-triangle', size: 20 })),
        React.createElement('span', { style: { fontWeight: 'var(--fw-label)', color: 'var(--text)' } }, 'Couldn’t load your rank data')),
      React.createElement('p', { key: 'p', style: { margin: '0 0 var(--sp-4)', color: 'var(--text-2)', fontSize: '.88rem', lineHeight: 1.5 } },
        (message || 'Something went wrong') + '. Check your connection and try again — your tracked keywords are safe.'),
      React.createElement('button', { key: 'b', type: 'button', className: 'wiz-btn wiz-btn-secondary wiz-focusable', onClick: onRetry },
        React.createElement(WIcon, { name: 'refresh-cw', size: 15 }), 'Retry'),
    ], { borderColor: 'color-mix(in oklch, var(--warn) 30%, var(--border))' }));
}

/* locked-upgrade — free tier: never hide the feature, de-emphasize + CTA */
function LockedUpgrade({ plan, onUpsell }) {
  return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 'var(--sp-5)' } }, Header(),
    Card([
      React.createElement('div', { key: 'ic', style: { width: 56, height: 56, margin: '0 auto var(--sp-4)', borderRadius: 'var(--r-lg)',
        background: 'var(--beam-dim)', color: 'var(--beam)', display: 'flex', alignItems: 'center', justifyContent: 'center' } }, React.createElement(WIcon, { name: 'trending-up', size: 28 })),
      React.createElement('h2', { key: 'h', className: 'wiz-title', style: { margin: '0 0 8px', fontSize: '1.25rem' } }, 'Rank tracking is included on paid plans'),
      React.createElement('p', { key: 'p', style: { margin: '0 auto var(--sp-5)', maxWidth: 460, color: 'var(--text-2)', lineHeight: 1.5, fontSize: '.9rem' } },
        'Track keyword positions weekly on Solo and above — with trend lines, SERP features, and movement alerts. Daily checks are a Starter+ add-on.'),
      React.createElement('div', { key: 'c', style: { display: 'flex', gap: 'var(--sp-3)', justifyContent: 'center', flexWrap: 'wrap' } },
        React.createElement('button', { type: 'button', className: 'wiz-btn wiz-btn-primary wiz-focusable', onClick: onUpsell },
          React.createElement(WIcon, { name: 'arrow-up-right', size: 15 }), 'See plans'),
        React.createElement('span', { style: { fontFamily: 'var(--font-mono)', fontSize: '.7rem', color: 'var(--dim)', alignSelf: 'center' } }, 'Current plan: ' + (plan || 'free'))),
    ], { textAlign: 'center', padding: 'var(--sp-7)' }));
}

/* freshness + honest coverage banner (paid Nerve surface — disclose lag) */
function FreshnessBanner({ meta }) {
  const rel = (meta && meta.checkedRel) || 'pending first check';
  const parts = [];
  if (meta && meta.truncated) parts.push('very large keyword set — a few latest positions may lag (' + meta.shown + ' of ' + meta.total + ' shown)');
  return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap',
    padding: '9px 14px', borderRadius: 'var(--r)', background: 'var(--surface-2)', border: '1px solid var(--border)',
    fontFamily: 'var(--font-mono)', fontSize: '.7rem', color: 'var(--muted)' } },
    React.createElement(WIcon, { name: 'clock', size: 12, style: { color: 'var(--beam)' } }),
    React.createElement('span', null, 'Positions as of ' + rel + ' · weekly cadence'),
    parts.length ? React.createElement('span', { style: { color: 'var(--dim)' } }, '· ' + parts.join(' · ')) : null,
    React.createElement('span', { style: { color: 'var(--dim)' } }, '· volume / difficulty / AI visibility not yet measured'));
}

/* add-keyword form (POST /seo/rank/targets) — honest 402 plan-gate handling */
function AddKeyword({ defaultDomain, onAdded, onCancel }) {
  const [domain, setDomain] = useStateS2(defaultDomain || '');
  const [kw, setKw] = useStateS2('');
  const [busy, setBusy] = useStateS2(false);
  const submit = async function () {
    const d = cleanDomain(domain), k = kw.trim();
    if (!d) { toast('Enter the site domain to track', 'beam'); return; }
    if (!k) { toast('Enter a keyword to track', 'beam'); return; }
    setBusy(true);
    try {
      const r = await fetch(apiBase() + '/seo/rank/targets', { method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + authToken() },
        body: JSON.stringify({ target_domain: d, keywords: [k], cadence: 'weekly' }) });
      let b = null; try { b = await r.json(); } catch (e) {}
      if (r.status === 402) { toast((b && b.error) || 'Upgrade required to track more keywords', 'beam'); setBusy(false); return; }
      if (!r.ok) { toast((b && b.error) || 'Could not add keyword', 'beam'); setBusy(false); return; }
      toast('Tracking “' + k + '” — first check runs shortly', 'ok');
      setKw(''); setBusy(false); if (onAdded) onAdded();
    } catch (e) { toast('Network error — try again', 'beam'); setBusy(false); }
  };
  const inp = { padding: '11px 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border-2)', background: 'var(--surface-2)',
    color: 'var(--text)', fontFamily: 'var(--font-body)', fontSize: '.88rem', outline: 'none', minWidth: 0 };
  return Card([
    React.createElement('div', { key: 'r', style: { display: 'flex', gap: 'var(--sp-3)', flexWrap: 'wrap', alignItems: 'center' } },
      React.createElement('input', { key: 'd', value: domain, onChange: e => setDomain(e.target.value), placeholder: 'example.com',
        'aria-label': 'Site domain', className: 'wiz-focusable', style: Object.assign({ flex: '1 1 180px' }, inp) }),
      React.createElement('input', { key: 'k', value: kw, onChange: e => setKw(e.target.value),
        onKeyDown: e => { if (e.key === 'Enter') submit(); }, placeholder: 'e.g. managed wordpress hosting',
        'aria-label': 'Keyword', className: 'wiz-focusable', style: Object.assign({ flex: '2 1 240px' }, inp) }),
      React.createElement('button', { key: 'b', type: 'button', disabled: busy, className: 'wiz-btn wiz-btn-primary wiz-focusable',
        onClick: submit, style: { padding: '11px 20px' } }, busy ? 'Adding…' : 'Track keyword'),
      onCancel ? React.createElement('button', { key: 'c', type: 'button', className: 'wiz-btn wiz-btn-secondary wiz-focusable', onClick: onCancel, style: { padding: '11px 16px' } }, 'Cancel') : null),
  ], { borderRadius: 'var(--r-lg)' });
}

/* detail-open loading: real header context + an honest spinner while the
   single per-target history call resolves — never the detail's "no checks
   recorded yet" copy for a keyword that HAS been checked (Radical Transparency). */
function DetailLoading({ kw, onBack }) {
  return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 'var(--sp-5)' } },
    React.createElement('button', { type: 'button', onClick: onBack, className: 'wiz-btn wiz-btn-secondary wiz-focusable',
      style: { display: 'inline-flex', alignItems: 'center', gap: 6, alignSelf: 'flex-start', fontSize: '.74rem', padding: '7px 14px' } },
      React.createElement(WIcon, { name: 'chevron-left', size: 15 }), 'All keywords'),
    Card(React.createElement('div', { role: 'status', 'aria-live': 'polite', style: { display: 'flex', alignItems: 'center', gap: 12, color: 'var(--text-2)' } },
      React.createElement('span', { className: 'wiz-spin', style: { width: 18, height: 18, borderRadius: '50%', border: '2px solid var(--border-2)', borderTopColor: 'var(--beam)', display: 'inline-block', animation: 'wpsbdspin 0.8s linear infinite' } }),
      React.createElement('span', { style: { fontFamily: 'var(--font-mono)', fontSize: '.85rem' } }, 'Loading position history for “' + (kw || 'keyword') + '”…'))));
}

function SeoSurface(props) {
  const live = window.WPSB.SeoLive.useSeoLiveData();
  const [view, setView] = useStateS2(null);    // null = overview, else keyword row
  const [viewLoading, setViewLoading] = useStateS2(false);   // detail history in-flight
  const [range, setRange] = useStateS2('30d');
  const [adding, setAdding] = useStateS2(false);
  const [refreshing, setRefreshing] = useStateS2(false);

  // seo.rank.viewed — overview opened/loaded (fired once on mount). Fire-and-forget.
  useEffectS2(function () { emitClientEvent('seo.rank.viewed', { surface: 'seo_tracker' }); }, []);

  const onUpsell = useCbS2(function () { toast('→ See plans for daily rank tracking + the SEO add-on', 'beam'); }, []);

  // Open a keyword detail: emit seo.keyword.opened, then lazily hydrate the
  // single target's position history (trend/prev/best) — 1 call, not N+1.
  const openKeyword = useCbS2(async function (row) {
    emitClientEvent('seo.keyword.opened', { keyword: row && row.kw, device: row && row._device });
    setView(row);                 // instant: header/position/serp already on the row
    setViewLoading(true);
    try {
      const hist = (live.fetchHistory && row && row._targetId) ? await live.fetchHistory(row._targetId) : null;
      if (hist) setView(function (cur) { return cur ? Object.assign({}, cur, { trend: hist.trend, prev: hist.prev, best: hist.best }) : cur; });
    } catch (_) { /* keep the overview row; history is best-effort */ }
    setViewLoading(false);
  }, [live]);
  const closeKeyword = useCbS2(function () { setView(null); setViewLoading(false); }, []);

  // run-now (execution spine) — POST /run-now then poll /batch, then reload
  const onRefresh = useCbS2(async function () {
    if (refreshing) return;
    setRefreshing(true);
    try {
      const r = await fetch(apiBase() + '/seo/rank/run-now', { method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + authToken() }, body: JSON.stringify({}) });
      let b = null; try { b = await r.json(); } catch (e) {}
      if (!r.ok) { toast((b && b.error) || 'Could not start a refresh', 'beam'); setRefreshing(false); return; }
      toast('Checking ' + (b && b.targets != null ? b.targets + ' ' : '') + 'keywords — results update shortly', 'ok');
      const batchId = b && b.batch_id;
      let polls = 0;
      const tick = async function () {
        polls++;
        try {
          if (batchId) {
            const pr = await fetch(apiBase() + '/seo/rank/batch/' + encodeURIComponent(batchId), { headers: { Authorization: 'Bearer ' + authToken() } });
            const pb = await pr.json().catch(function () { return {}; });
            if (pb && pb.complete) { setRefreshing(false); live.reload(); return; }
          }
        } catch (e) { /* keep polling */ }
        if (polls < 10) { setTimeout(tick, 6000); } else { setRefreshing(false); live.reload(); }
      };
      setTimeout(tick, 6000);
    } catch (e) { toast('Network error — try again', 'beam'); setRefreshing(false); }
  }, [refreshing, live]);

  // ── discriminated render ──
  if (live.status === 'loading') return React.createElement(LoadingState);
  if (live.status === 'error')   return React.createElement(ErrorState, { message: live.error, onRetry: live.reload });
  if (live.status === 'locked')  return React.createElement(LockedUpgrade, { plan: live.allowance && live.allowance.plan, onUpsell: onUpsell });

  if (live.status === 'empty') {
    return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 'var(--sp-5)' } },
      Header(),
      Card([
        React.createElement('div', { key: 'ic', style: { width: 56, height: 56, margin: '0 auto var(--sp-4)', borderRadius: 'var(--r-lg)', background: 'var(--beam-dim)', color: 'var(--beam)', display: 'flex', alignItems: 'center', justifyContent: 'center' } }, React.createElement(WIcon, { name: 'search', size: 28 })),
        React.createElement('h2', { key: 'h', className: 'wiz-title', style: { margin: '0 0 8px', fontSize: '1.25rem' } }, 'Track your first keyword'),
        React.createElement('p', { key: 'p', style: { margin: '0 auto var(--sp-5)', maxWidth: 460, color: 'var(--text-2)', lineHeight: 1.5, fontSize: '.9rem' } },
          'Add a site and the search terms you want to rank for. We check positions weekly, track movement, and chart the trend.'),
      ], { textAlign: 'center', padding: 'var(--sp-6) var(--sp-6) var(--sp-5)' }),
      React.createElement(AddKeyword, { defaultDomain: '', onAdded: live.reload }));
  }

  // ready
  const meta = live.meta || {};
  const planMode = meta.daily ? 'daily' : 'weekly-eligible';   // weekly tracking; daily upsell shown

  let body;
  if (view) {
    body = viewLoading
      ? React.createElement(DetailLoading, { kw: view.kw, onBack: closeKeyword })
      : React.createElement(window.WPSB.SeoDetail.SeoKeywordDetail, { row: view, plan: planMode, meta: meta,
          onBack: closeKeyword, onUpsell: onUpsell });
  } else {
    body = React.createElement(React.Fragment, null,
      React.createElement(FreshnessBanner, { meta: meta }),
      adding ? React.createElement(AddKeyword, { defaultDomain: meta.domain || '', onAdded: function () { setAdding(false); live.reload(); }, onCancel: function () { setAdding(false); } }) : null,
      React.createElement(window.WPSB.SeoTracker.SeoTrackerOverview, {
        layout: 'table-first', plan: planMode, aiLocked: true, range: range, setRange: setRange,
        keywords: live.keywords, stats: live.stats, competitors: live.competitors, suggestions: live.suggestions, meta: meta,
        onOpenKeyword: openKeyword, onUpsell: onUpsell, onTrackKeyword: function () { setAdding(true); },
        onRefresh: onRefresh, refreshing: refreshing }));
  }

  const geo = (!view && window.GeoVisibility) ? React.createElement(window.GeoVisibility, { onUpsell: onUpsell }) : null;
  return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 'var(--sp-5)' } }, body, geo);
}

window.SeoSurface = SeoSurface;
window.WPSB = window.WPSB || {};
window.WPSB.SeoSurface = SeoSurface;
console.log('[WPSB] SeoSurface v2.1.0 loaded (bulk /seo/rank/latest + client-event emits)');
})();
