/* Slate — Alerts (Phase 2)
   ===================================================
   Single-screen UI with three tabs:

     Rules      — list + CRUD on /api/analytics/alerts (rules engine)
     Feed       — recent fires from /api/analytics/alert-events with ack
     Anomalies  — read-only z-score outliers from /api/analytics/anomalies
                  with a "create alert from this" quick-action

   Capability matrix (matches src/capabilities.js + Auth helpers):
     - analytics.view_alerts      → can see this screen at all
     - analytics.manage_alerts    → can create/edit/delete/pause rules,
                                     and acknowledge fires

   The route lives at /cms/#alerts. Sidebar gating in sidebar.jsx adds
   the entry to NAV_INSIGHTS for roles that have view_alerts. */

(function () {
  /* Backend metric ids → human label + suggested comparator/threshold.
     The list mirrors what the metrics rollup endpoint returns plus the
     two infra metrics (screens_online, active_alerts) that are useful
     to alert on. Adding a new metric here only adds it to the picker;
     the backend stores any string. */
  const ALERT_METRICS = [
    { id: 'impressions',     label: 'Impressions (window total)',     unit: '',   suggest: { comparator: 'lt', threshold: 100 } },
    { id: 'scans',           label: 'QR scans',                       unit: '',   suggest: { comparator: 'lt', threshold: 5 } },
    { id: 'scan_rate',       label: 'Scan rate',                      unit: '%',  suggest: { comparator: 'lt', threshold: 1.5 } },
    { id: 'cta_clicks',      label: 'CTA clicks',                     unit: '',   suggest: { comparator: 'lt', threshold: 10 } },
    { id: 'screens_online',  label: 'Screens online',                 unit: '',   suggest: { comparator: 'lt', threshold: 1 } },
    { id: 'screens_offline', label: 'Screens offline',                unit: '',   suggest: { comparator: 'gt', threshold: 0 } },
    { id: 'revenue_cents',   label: 'Attributed revenue (£, in pence)', unit: '£', suggest: { comparator: 'lt', threshold: 0 } },
    { id: 'anomaly_zscore',  label: 'Activity anomaly (|z| score)',   unit: 'σ',  suggest: { comparator: 'gt', threshold: 2.5 } },
  ];

  /* Channel ids + a one-line UX description. Backend stores `channel`
     as a string, so anything goes — these are the curated options the
     UI exposes. The actual delivery integration is wired separately
     (out of scope for this UI; rules are authored here). */
  const ALERT_CHANNELS = [
    { id: 'inapp',   label: 'In-app',  hint: 'Bell icon + Live event log',                requiresTarget: false },
    { id: 'email',   label: 'Email',   hint: 'Comma-separated addresses',                  requiresTarget: true,  placeholder: 'ops@aquarium.com, marketing@…' },
    { id: 'slack',   label: 'Slack',   hint: 'Channel name or webhook URL',                requiresTarget: true,  placeholder: '#alerts or https://hooks.slack.com/…' },
    { id: 'teams',   label: 'MS Teams', hint: 'Webhook URL from your Teams channel',       requiresTarget: true,  placeholder: 'https://outlook.office.com/webhook/…' },
    { id: 'webhook', label: 'Webhook', hint: 'POST JSON to your endpoint',                 requiresTarget: true,  placeholder: 'https://example.com/aquaos-alert' },
    { id: 'sms',     label: 'SMS',     hint: 'Phone numbers (E.164, comma separated)',     requiresTarget: true,  placeholder: '+447700900123, +447700900456' },
    { id: 'pagerduty', label: 'PagerDuty', hint: 'Events API v2 routing key (NOT API token)', requiresTarget: true,  placeholder: '32-char integration key' },
    { id: 'opsgenie',  label: 'Opsgenie',  hint: 'API integration key (set OPSGENIE_REGION=eu for EU accounts)', requiresTarget: true,  placeholder: 'GenieKey value' },
  ];

  /* Comparator picker — display in plain English. */
  const COMPARATORS = [
    { id: 'lt', label: 'is less than',    glyph: '<' },
    { id: 'gt', label: 'is greater than', glyph: '>' },
    { id: 'eq', label: 'is exactly',      glyph: '=' },
  ];

  /* ─── Helpers ─────────────────────────────────────────────────── */
  const _fmtN = typeof fmtN === 'function' ? fmtN : (n) => String(Math.round(Number(n) || 0));

  function metricMeta(id) {
    return ALERT_METRICS.find((m) => m.id === id) || { id, label: id, unit: '', suggest: {} };
  }
  function channelMeta(id) {
    return ALERT_CHANNELS.find((c) => c.id === id) || { id, label: id, hint: '' };
  }
  function fmtRelative(iso) {
    if (!iso) return 'never';
    const t = new Date(iso).getTime();
    if (!t) return iso;
    const s = Math.floor((Date.now() - t) / 1000);
    if (s < 60) return s + 's ago';
    if (s < 3600) return Math.floor(s / 60) + 'm ago';
    if (s < 86400) return Math.floor(s / 3600) + 'h ago';
    return Math.floor(s / 86400) + 'd ago';
  }

  /* ─── Tab strip ───────────────────────────────────────────────── */
  function TabStrip({ tabs, active, onSelect, badges }) {
    return (
      <div className="x-tabstrip" style={{
        display: 'flex', gap: 4, borderBottom: '1px solid var(--aqos-border)',
        marginBottom: 16, paddingBottom: 0,
      }}>
        {tabs.map((t) => {
          const isActive = active === t.id;
          return (
            <a
              key={t.id}
              onClick={() => onSelect(t.id)}
              style={{
                padding: '10px 14px',
                fontSize: 12.5,
                fontWeight: isActive ? 600 : 500,
                color: isActive ? 'var(--aqos-text)' : 'var(--aqos-text-dim)',
                borderBottom: isActive ? '2px solid var(--aqos-accent, oklch(0.66 0.16 270))' : '2px solid transparent',
                cursor: 'pointer',
                marginBottom: -1,
              }}
            >
              {t.label}
              {badges && badges[t.id] != null && (
                <span style={{
                  marginLeft: 6, padding: '1px 6px', borderRadius: 8,
                  background: 'var(--aqos-surface-2)', fontSize: 10.5, color: 'var(--aqos-text-faint)',
                }}>{badges[t.id]}</span>
              )}
            </a>
          );
        })}
      </div>
    );
  }

  /* ─── Rules tab ───────────────────────────────────────────────── */
  function RulesTab({ canManage }) {
    const [rules, setRules] = useState([]);
    const [loading, setLoading] = useState(true);
    const [editing, setEditing] = useState(null); // alert object or 'new'
    const [err, setErr] = useState(null);
    const [expandedId, setExpandedId] = useState(null); // rule id with dispatch history open
    const [dispatches, setDispatches] = useState({}); // { ruleId: [rows] }

    function reload() {
      setLoading(true);
      apiFetch('/api/analytics/alerts')
        .then((r) => { setRules((r && r.alerts) || []); setLoading(false); })
        .catch((e) => { setErr(e.message); setLoading(false); });
    }
    useEffect(() => { reload(); }, []);

    function toggleStatus(rule) {
      const next = rule.status === 'active' ? 'paused' : 'active';
      apiFetch('/api/analytics/alerts/' + encodeURIComponent(rule.id), {
        method: 'PATCH',
        body: JSON.stringify({ status: next }),
      }).then(reload).catch((e) => window.toast && window.toast(e.message, 'error'));
    }
    function deleteRule(rule) {
      if (!window.confirm(`Delete alert "${rule.name}"? Past fires are kept.`)) return;
      apiFetch('/api/analytics/alerts/' + encodeURIComponent(rule.id), { method: 'DELETE' })
        .then(reload).catch((e) => window.toast && window.toast(e.message, 'error'));
    }
    /* Manual test-fire — authors can verify their channel + destination
       work before relying on the rule. Dispatches the alert via the
       backend exactly as a real fire would, but flags context.source =
       'test' so the Recent fires tab can distinguish them. */
    function toggleHistory(rule) {
      if (expandedId === rule.id) { setExpandedId(null); return; }
      setExpandedId(rule.id);
      if (!dispatches[rule.id]) {
        apiFetch('/api/analytics/alerts/' + encodeURIComponent(rule.id) + '/dispatches?limit=10')
          .then((r) => setDispatches((prev) => ({ ...prev, [rule.id]: (r && r.dispatches) || [] })))
          .catch((e) => window.toast && window.toast(e.message, 'error'));
      }
    }
    function cloneRule(rule) {
      apiFetch('/api/analytics/alerts/' + encodeURIComponent(rule.id) + '/clone', { method: 'POST' })
        .then(() => { window.toast && window.toast(`Cloned "${rule.name}" — opens paused`); reload(); })
        .catch((e) => window.toast && window.toast('Clone failed: ' + e.message, 'error'));
    }
    function testRule(rule) {
      apiFetch('/api/analytics/alerts/' + encodeURIComponent(rule.id) + '/test', { method: 'POST' })
        .then((r) => {
          const msg = r.ok
            ? `Sent test → ${rule.channel}${r.info ? ' (' + r.info + ')' : ''}`
            : `Test failed: ${r.error || 'unknown error'}`;
          window.toast && window.toast(msg, r.ok ? 'success' : 'error');
          reload();
        })
        .catch((e) => window.toast && window.toast('Test failed: ' + e.message, 'error'));
    }

    return (
      <div>
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          marginBottom: 12,
        }}>
          <div style={{ color: 'var(--aqos-text-dim)', fontSize: 12.5 }}>
            {loading ? 'Loading…' : `${rules.length} alert ${rules.length === 1 ? 'rule' : 'rules'}`}
          </div>
          {canManage && (
            <button className="x-btn" onClick={() => setEditing('new')}>+ New alert</button>
          )}
        </div>

        <div className="x-analytics-panel" style={{ padding: 0 }}>
          <table className="x-analytics-table">
            <thead>
              <tr>
                <th style={{ width: 24 }}></th>
                <th>Name</th>
                <th>Condition</th>
                <th>Window</th>
                <th>Channel</th>
                <th style={{ textAlign: 'right' }}>Last fired</th>
                {canManage && <th style={{ width: 120 }}></th>}
              </tr>
            </thead>
            <tbody>
              {!loading && rules.length === 0 && (
                <tr><td colSpan={canManage ? 7 : 6} style={{ padding: '36px 14px', textAlign: 'center', color: 'var(--aqos-text-faint)' }}>
                  No alert rules yet. {canManage ? <a onClick={() => setEditing('new')} style={{ cursor: 'pointer', color: 'var(--aqos-accent, oklch(0.66 0.16 270))' }}>Create your first one →</a> : null}
                </td></tr>
              )}
              {rules.map((r) => {
                const m = metricMeta(r.metric);
                const c = COMPARATORS.find((x) => x.id === r.comparator) || COMPARATORS[0];
                const ch = channelMeta(r.channel);
                const isActive = r.status === 'active';
                const expanded = expandedId === r.id;
                return (<Fragment key={r.id}>
                  <tr style={expanded ? { background: 'var(--aqos-surface-2)' } : null}>
                    <td>
                      <span title={r.status} style={{
                        display: 'inline-block', width: 8, height: 8, borderRadius: 4,
                        background: isActive ? 'oklch(0.78 0.13 160)' : 'var(--aqos-text-faint)',
                      }} />
                    </td>
                    <td>
                      <a onClick={() => toggleHistory(r)} style={{ cursor: 'pointer' }} title="Show dispatch history">
                        <span style={{ display: 'inline-block', width: 10, color: 'var(--aqos-text-faint)', fontSize: 9 }}>{expanded ? '▾' : '▸'}</span>
                        <strong>{r.name}</strong>
                      </a>
                    </td>
                    <td>
                      <span style={{ color: 'var(--aqos-text-dim)' }}>
                        {m.label} <span style={{ color: 'var(--aqos-text-faint)' }}>{c.label}</span> <strong>{r.threshold ?? '—'}{m.unit}</strong>
                      </span>
                    </td>
                    <td><span style={{ color: 'var(--aqos-text-dim)' }}>{r.window_minutes}m</span></td>
                    <td>
                      <span style={{ color: 'var(--aqos-text-dim)' }}>{ch.label}</span>
                      {r.channel_target && <span style={{ marginLeft: 6, color: 'var(--aqos-text-faint)', fontSize: 11 }}>· {r.channel_target}</span>}
                    </td>
                    <td style={{ textAlign: 'right', color: 'var(--aqos-text-faint)', fontSize: 11.5 }}>
                      {fmtRelative(r.last_fired_at)}
                    </td>
                    {canManage && (
                      <td style={{ textAlign: 'right' }}>
                        <a onClick={() => cloneRule(r)} style={{ marginRight: 8, fontSize: 11, color: 'var(--aqos-text-dim)', cursor: 'pointer' }} title="Clone this rule (paused)">Clone</a>
                        <a onClick={() => testRule(r)} style={{ marginRight: 8, fontSize: 11, color: 'var(--aqos-text-dim)', cursor: 'pointer' }} title={`Send a test fire via ${r.channel}`}>Test</a>
                        <a onClick={() => toggleStatus(r)} style={{ marginRight: 8, fontSize: 11, color: 'var(--aqos-text-dim)', cursor: 'pointer' }}>
                          {isActive ? 'Pause' : 'Resume'}
                        </a>
                        <a onClick={() => setEditing(r)} style={{ marginRight: 8, fontSize: 11, cursor: 'pointer' }}>Edit</a>
                        <a onClick={() => deleteRule(r)} style={{ fontSize: 11, color: 'var(--aqos-danger)', cursor: 'pointer' }}>Delete</a>
                      </td>
                    )}
                  </tr>
                  {expanded && <DispatchHistoryRow ruleId={r.id} dispatches={dispatches[r.id]} canManage={canManage} />}
                </Fragment>);
              })}
            </tbody>
          </table>
        </div>

        {editing && (
          <AlertRuleModal
            initial={editing === 'new' ? null : editing}
            onCancel={() => setEditing(null)}
            onSaved={() => { setEditing(null); reload(); }}
          />
        )}

        {err && (
          <div style={{
            marginTop: 16, padding: '10px 14px', fontSize: 12.5,
            background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
            border: '1px solid color-mix(in srgb, var(--aqos-danger) 32%, transparent)',
            borderRadius: 8,
          }}>
            <strong>Couldn't load alerts:</strong> {err}
          </div>
        )}
      </div>
    );
  }

  /* Inline expansion under a rule row showing recent dispatch attempts. */
  function DispatchHistoryRow({ ruleId, dispatches, canManage }) {
    const colspan = canManage ? 7 : 6;
    return (
      <tr>
        <td colSpan={colspan} style={{ padding: '0 0 14px 38px', background: 'var(--aqos-surface-2)' }}>
          <div style={{ fontSize: 11, color: 'var(--aqos-text-faint)', textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 6, paddingTop: 8 }}>
            Last 10 dispatch attempts
          </div>
          {!dispatches && <div style={{ padding: '8px 0', color: 'var(--aqos-text-faint)', fontSize: 12 }}>Loading…</div>}
          {dispatches && dispatches.length === 0 && (
            <div style={{ padding: '8px 0', color: 'var(--aqos-text-faint)', fontSize: 12 }}>No dispatches yet — when this rule fires (or you Test it), the result lands here.</div>
          )}
          {dispatches && dispatches.length > 0 && (
            <div style={{ display: 'grid', gap: 3 }}>
              {dispatches.map((d) => (
                <div key={d.id} style={{
                  display: 'grid',
                  gridTemplateColumns: 'auto 130px 90px 1fr',
                  gap: 10, alignItems: 'center',
                  fontSize: 11.5,
                  padding: '4px 0',
                }}>
                  <span style={{
                    width: 7, height: 7, borderRadius: 4,
                    background: d.ok ? 'oklch(0.78 0.13 160)' : 'var(--aqos-danger)',
                  }} />
                  <span style={{ color: 'var(--aqos-text-faint)' }}>{new Date(d.attempted_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })}</span>
                  <span style={{ color: 'var(--aqos-text-dim)' }}>{d.channel}</span>
                  <span style={{ color: d.ok ? 'var(--aqos-text)' : 'var(--aqos-danger)', fontFamily: 'var(--aq-ff-mono, ui-monospace, monospace)' }}>
                    {d.ok ? (d.info || 'ok') : (d.error || 'failed')}
                  </span>
                </div>
              ))}
            </div>
          )}
        </td>
      </tr>
    );
  }

  /* ─── Create / edit modal ─────────────────────────────────────── */
  function AlertRuleModal({ initial, onCancel, onSaved }) {
    /* Edit only when we have a real row id. The Anomalies/Suggestions tabs
       open this modal with a PREFILLED-but-id-less `initial` to seed a NEW
       rule; keying off `!!initial` (truthy for that seed object) made submit
       PATCH /alerts/undefined instead of POSTing. Match the id-based check
       ScheduledReportModal already uses. */
    const isEdit = !!(initial && initial.id);
    const [name, setName]           = useState(initial?.name || '');
    const [metric, setMetric]       = useState(initial?.metric || ALERT_METRICS[0].id);
    const [comparator, setComparator] = useState(initial?.comparator || ALERT_METRICS[0].suggest.comparator);
    const [threshold, setThreshold] = useState(initial?.threshold ?? ALERT_METRICS[0].suggest.threshold);
    const [windowMin, setWindowMin] = useState(initial?.window_minutes || 60);
    const [channel, setChannel]     = useState(initial?.channel || 'inapp');
    const [target, setTarget]       = useState(initial?.channel_target || '');
    const [busy, setBusy] = useState(false);
    const [err, setErr] = useState(null);

    /* When the metric changes, suggest sensible default comparator + threshold
       (only if user hasn't manually edited yet — i.e. when those fields still
       hold the previous metric's defaults). */
    function onMetricChange(id) {
      setMetric(id);
      const meta = metricMeta(id);
      if (meta.suggest.comparator) setComparator(meta.suggest.comparator);
      if (meta.suggest.threshold != null) setThreshold(meta.suggest.threshold);
    }

    function submit(e) {
      if (e) e.preventDefault();
      if (!name.trim()) { setErr('Name required'); return; }
      const ch = channelMeta(channel);
      if (ch.requiresTarget && !target.trim()) { setErr(ch.label + ' channel needs a destination'); return; }
      setBusy(true); setErr(null);
      const body = {
        name: name.trim(),
        metric, comparator,
        threshold: threshold === '' ? null : Number(threshold),
        window_minutes: Number(windowMin) || 60,
        channel,
        channel_target: ch.requiresTarget ? target.trim() : null,
      };
      const req = isEdit
        ? apiFetch('/api/analytics/alerts/' + encodeURIComponent(initial.id), { method: 'PATCH', body: JSON.stringify(body) })
        : apiFetch('/api/analytics/alerts', { method: 'POST', body: JSON.stringify(body) });
      req.then(() => { setBusy(false); onSaved(); })
         .catch((e) => { setBusy(false); setErr(e.message); });
    }

    const ch = channelMeta(channel);
    const m  = metricMeta(metric);

    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 200,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }} onClick={onCancel}>
        <form onSubmit={submit} onClick={(e) => e.stopPropagation()} style={{
          width: 540, maxHeight: '90vh', overflowY: 'auto',
          background: 'var(--aqos-surface)', borderRadius: 10,
          border: '1px solid var(--aqos-border)', padding: 22,
          boxShadow: '0 20px 50px rgba(0,0,0,0.35)',
        }}>
          <h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 4 }}>
            {isEdit ? 'Edit alert rule' : 'New alert rule'}
          </h2>
          <p style={{ fontSize: 12, color: 'var(--aqos-text-faint)', marginBottom: 16 }}>
            Watches one metric in a sliding window. Fires at most once per window.
          </p>

          <Field label="Name">
            <input value={name} onChange={(e) => setName(e.target.value)}
                   placeholder="e.g. Lobby screen offline > 5m"
                   style={inputStyle} autoFocus />
          </Field>

          <Field label="Metric">
            <select value={metric} onChange={(e) => onMetricChange(e.target.value)} style={inputStyle}>
              {ALERT_METRICS.map((opt) => (
                <option key={opt.id} value={opt.id}>{opt.label}</option>
              ))}
            </select>
          </Field>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
            <Field label="Comparator">
              <select value={comparator} onChange={(e) => setComparator(e.target.value)} style={inputStyle}>
                {COMPARATORS.map((c) => <option key={c.id} value={c.id}>{c.glyph} {c.label}</option>)}
              </select>
            </Field>
            <Field label={`Threshold ${m.unit ? '(' + m.unit + ')' : ''}`}>
              <input type="number" value={threshold ?? ''} onChange={(e) => setThreshold(e.target.value)} style={inputStyle} />
            </Field>
            <Field label="Window (min)">
              <input type="number" min="1" value={windowMin} onChange={(e) => setWindowMin(e.target.value)} style={inputStyle} />
            </Field>
          </div>

          <Field label="Notify via">
            <select value={channel} onChange={(e) => setChannel(e.target.value)} style={inputStyle}>
              {ALERT_CHANNELS.map((c) => <option key={c.id} value={c.id}>{c.label}</option>)}
            </select>
            <p style={{ marginTop: 4, fontSize: 11, color: 'var(--aqos-text-faint)' }}>{ch.hint}</p>
          </Field>

          {ch.requiresTarget && (
            <Field label="Destination">
              <input value={target} onChange={(e) => setTarget(e.target.value)}
                     placeholder={ch.placeholder} style={inputStyle} />
              {channel === 'email' && target.trim() && (
                <SmtpTestButton to={target.split(',')[0].trim()} />
              )}
            </Field>
          )}

          <div style={{
            marginTop: 14, padding: '10px 12px', fontSize: 12.5,
            background: 'var(--aqos-surface-2)', borderRadius: 6,
            color: 'var(--aqos-text-dim)',
          }}>
            <strong style={{ color: 'var(--aqos-text)' }}>Preview:</strong>{' '}
            When <em>{m.label}</em> {(COMPARATORS.find((c) => c.id === comparator) || {}).label} <strong>{threshold || '?'}{m.unit}</strong>{' '}
            in any <strong>{windowMin}-minute</strong> window, send to <strong>{ch.label}</strong>{ch.requiresTarget && target ? <> · <code style={{ fontSize: 11.5 }}>{target}</code></> : ''}.
          </div>

          {err && (
            <div style={{
              marginTop: 12, padding: '8px 12px', fontSize: 12.5,
              background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
              color: 'var(--aqos-danger)', borderRadius: 6,
            }}>{err}</div>
          )}

          <div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
            <button type="button" className="x-btn ghost" onClick={onCancel}>Cancel</button>
            <button type="submit" className="x-btn" disabled={busy || !name.trim()}>
              {busy ? 'Saving…' : (isEdit ? 'Save changes' : 'Create alert')}
            </button>
          </div>
        </form>
      </div>
    );
  }

  /* SmtpTestButton — pings /api/analytics/test-smtp with the address
     in the modal so authors can verify their config end-to-end. Used
     by both the alert rule modal (channel=email) and the scheduled-
     report modal. */
  function SmtpTestButton({ to }) {
    const [busy, setBusy] = useState(false);
    const [result, setResult] = useState(null);
    function go() {
      if (!to) return;
      setBusy(true); setResult(null);
      apiFetch('/api/analytics/test-smtp', {
        method: 'POST',
        body: JSON.stringify({ to }),
      })
        .then((r) => { setBusy(false); setResult({ ok: r.ok, msg: r.ok ? `Sent to ${to}` : (r.error || 'failed') }); })
        .catch((e) => { setBusy(false); setResult({ ok: false, msg: e.message }); });
    }
    return (
      <div style={{ marginTop: 6 }}>
        <a onClick={go} style={{ fontSize: 11, cursor: busy ? 'wait' : 'pointer', color: 'var(--aqos-text-dim)' }}>
          {busy ? 'Sending test…' : '↗ Send test email'}
        </a>
        {result && (
          <span style={{
            marginLeft: 10, fontSize: 11,
            color: result.ok ? 'oklch(0.78 0.13 160)' : 'var(--aqos-danger)',
          }}>
            {result.ok ? '✓' : '✕'} {result.msg}
          </span>
        )}
      </div>
    );
  }

  /* Form-row helper to keep the modal markup tidy. */
  function Field({ label, children }) {
    return (
      <label style={{ display: 'block', marginBottom: 12 }}>
        <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>{label}</div>
        {children}
      </label>
    );
  }
  const inputStyle = {
    width: '100%', padding: '8px 10px', fontSize: 13,
    background: 'var(--aqos-surface-2)', color: 'var(--aqos-text)',
    border: '1px solid var(--aqos-border)', borderRadius: 6,
    boxSizing: 'border-box',
  };

  /* ─── Feed tab ────────────────────────────────────────────────── */
  function FeedTab({ canManage }) {
    const [events, setEvents] = useState([]);
    const [loading, setLoading] = useState(true);
    const [err, setErr] = useState(null);

    function reload() {
      setLoading(true);
      apiFetch('/api/analytics/alert-events?limit=100')
        .then((r) => { setEvents((r && r.events) || []); setLoading(false); })
        .catch((e) => { setErr(e.message); setLoading(false); });
    }
    useEffect(() => { reload(); }, []);

    function ack(ev) {
      apiFetch('/api/analytics/alert-events/' + encodeURIComponent(ev.id) + '/ack', { method: 'POST' })
        .then(reload)
        .catch((e) => window.toast && window.toast(e.message, 'error'));
    }
    function ackAll() {
      const unack = events.filter(e => !e.acknowledged_at).length;
      if (unack === 0) { window.toast && window.toast('No open fires to acknowledge'); return; }
      if (!window.confirm(`Acknowledge all ${unack} open fire${unack === 1 ? '' : 's'}?`)) return;
      apiFetch('/api/analytics/alert-events/ack-all', { method: 'POST' })
        .then((r) => { window.toast && window.toast(`Acknowledged ${r.acknowledged} fire${r.acknowledged === 1 ? '' : 's'}`); reload(); })
        .catch((e) => window.toast && window.toast(e.message, 'error'));
    }

    return (
      <div>
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          marginBottom: 12,
        }}>
          <div style={{ color: 'var(--aqos-text-dim)', fontSize: 12.5 }}>
            {loading ? 'Loading…' : `${events.length} fire${events.length === 1 ? '' : 's'}`}
          </div>
          <span style={{ display: 'flex', gap: 12 }}>
            {canManage && events.some(e => !e.acknowledged_at) && (
              <a onClick={ackAll} style={{ fontSize: 12, cursor: 'pointer', color: 'var(--aqos-text-dim)' }} title="Mark every open fire as acknowledged">✓ Ack all</a>
            )}
            <a onClick={reload} style={{ fontSize: 12, cursor: 'pointer', color: 'var(--aqos-text-dim)' }}>↻ Refresh</a>
          </span>
        </div>

        <div className="x-analytics-panel" style={{ padding: 0 }}>
          <table className="x-analytics-table">
            <thead>
              <tr>
                <th style={{ width: 24 }}></th>
                <th>When</th>
                <th>Alert</th>
                <th>Observed</th>
                <th>Threshold</th>
                <th>Status</th>
                {canManage && <th style={{ width: 80 }}></th>}
              </tr>
            </thead>
            <tbody>
              {!loading && events.length === 0 && (
                <tr><td colSpan={canManage ? 7 : 6} style={{ padding: '36px 14px', textAlign: 'center', color: 'var(--aqos-text-faint)' }}>
                  No alerts have fired yet.
                </td></tr>
              )}
              {events.map((e) => {
                const acked = !!e.acknowledged_at;
                const m = metricMeta(e.metric);
                let ctx = null;
                try { ctx = e.context_json ? JSON.parse(e.context_json) : null; } catch (_) {}
                const isTest = ctx && ctx.source === 'test';
                return (
                  <tr key={e.id} style={acked ? { opacity: 0.55 } : null}>
                    <td>
                      <span style={{
                        display: 'inline-block', width: 8, height: 8, borderRadius: 4,
                        background: acked ? 'var(--aqos-text-faint)' : (isTest ? 'oklch(0.66 0.16 270)' : 'oklch(0.70 0.16 25)'),
                      }} />
                    </td>
                    <td style={{ color: 'var(--aqos-text-faint)', fontSize: 11.5 }}>
                      {fmtRelative(e.fired_at)}
                    </td>
                    <td>
                      <strong>{e.alert_name}</strong>
                      <span style={{ color: 'var(--aqos-text-faint)', fontSize: 11 }}> · {m.label}</span>
                      {isTest && <span style={{ marginLeft: 6, padding: '1px 5px', fontSize: 10, borderRadius: 3, background: 'color-mix(in srgb, oklch(0.66 0.16 270) 18%, transparent)', color: 'oklch(0.66 0.16 270)', fontWeight: 600 }}>TEST</span>}
                    </td>
                    <td><strong>{e.observed_value != null ? _fmtN(e.observed_value) + (m.unit || '') : '—'}</strong></td>
                    <td style={{ color: 'var(--aqos-text-dim)' }}>{e.threshold != null ? _fmtN(e.threshold) + (m.unit || '') : '—'}</td>
                    <td>
                      {acked ? (
                        <span style={{ color: 'var(--aqos-text-faint)', fontSize: 11.5 }}>Acknowledged {fmtRelative(e.acknowledged_at)}</span>
                      ) : (
                        <span style={{ color: 'oklch(0.70 0.16 25)', fontSize: 11.5, fontWeight: 600 }}>Open</span>
                      )}
                    </td>
                    {canManage && (
                      <td style={{ textAlign: 'right' }}>
                        {!acked && <a onClick={() => ack(e)} style={{ fontSize: 11, cursor: 'pointer' }}>Ack</a>}
                      </td>
                    )}
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>

        {err && (
          <div style={{
            marginTop: 16, padding: '10px 14px', fontSize: 12.5,
            background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
            border: '1px solid color-mix(in srgb, var(--aqos-danger) 32%, transparent)',
            borderRadius: 8,
          }}>
            <strong>Couldn't load events:</strong> {err}
          </div>
        )}
      </div>
    );
  }

  /* ─── Suggestion card ─────────────────────────────────────────── */
  function SuggestionCard({ s, canManage, onCreate, onDismiss }) {
    const sev = s.severity || 'info';
    const tone = sev === 'crit' ? 'var(--aqos-danger)' : (sev === 'warn' ? 'oklch(0.78 0.14 75)' : 'oklch(0.66 0.16 270)');
    const sevLabel = sev === 'crit' ? 'CRITICAL' : (sev === 'warn' ? 'WARN' : 'INFO');
    return (
      <div style={{
        padding: '14px 16px',
        background: 'var(--aqos-surface)',
        border: '1px solid var(--aqos-border)',
        borderLeft: `3px solid ${tone}`,
        borderRadius: 8,
        display: 'grid', gridTemplateColumns: '1fr auto', gap: 12,
        alignItems: 'flex-start',
      }}>
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
            <span style={{
              padding: '1px 6px', borderRadius: 3, fontSize: 10, fontWeight: 600,
              background: `color-mix(in srgb, ${tone} 15%, transparent)`, color: tone,
              letterSpacing: 0.4,
            }}>{sevLabel}</span>
            <strong style={{ fontSize: 13 }}>{s.title}</strong>
          </div>
          <div style={{ color: 'var(--aqos-text-dim)', fontSize: 12.5, lineHeight: 1.55 }}>
            {s.summary}
          </div>
          <div style={{ marginTop: 8, fontSize: 11.5, color: 'var(--aqos-text-faint)' }}>
            <strong>Suggested rule:</strong> {s.suggested_rule.metric} {s.suggested_rule.comparator} {s.suggested_rule.threshold} over {s.suggested_rule.window_minutes}m → {s.suggested_rule.channel}
          </div>
        </div>
        <div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
          {canManage && (
            <button className="x-btn" onClick={onCreate} style={{ padding: '6px 10px', fontSize: 11.5 }}>+ Create alert</button>
          )}
          <button className="x-btn ghost" onClick={onDismiss} style={{ padding: '6px 10px', fontSize: 11.5 }} title="Hide this suggestion (only you)">Dismiss</button>
        </div>
      </div>
    );
  }

  /* ─── Anomalies tab ───────────────────────────────────────────── */
  function AnomaliesTab({ canManage }) {
    const [anomalies, setAnomalies] = useState([]);
    const [meta, setMeta] = useState({});
    const [loading, setLoading] = useState(true);
    const [err, setErr] = useState(null);
    const [createFrom, setCreateFrom] = useState(null); // anomaly to convert to alert

    /* Suggestions live alongside the raw anomaly grid because they're
       the actionable counterpart — anomalies show "what" happened,
       suggestions show "what to do about it". */
    const [suggestions, setSuggestions] = useState([]);
    const [suggestionsLoading, setSuggestionsLoading] = useState(true);
    const [suggestionsErr, setSuggestionsErr] = useState(null);

    useEffect(() => {
      apiFetch('/api/analytics/anomalies?lookback_hours=24&baseline_days=7')
        .then((r) => { setAnomalies((r && r.anomalies) || []); setMeta(r || {}); setLoading(false); })
        .catch((e) => { setErr(e.message); setLoading(false); });
    }, []);
    useEffect(() => {
      apiFetch('/api/analytics/alert-suggestions')
        .then((r) => { setSuggestions((r && r.suggestions) || []); setSuggestionsLoading(false); })
        .catch((e) => { setSuggestionsErr(e.message); setSuggestionsLoading(false); });
    }, []);

    function dismissSuggestion(s) {
      apiFetch('/api/analytics/alert-suggestions/' + encodeURIComponent(s.key) + '/dismiss', { method: 'POST' })
        .then(() => setSuggestions((prev) => prev.filter(x => x.key !== s.key)))
        .catch((e) => window.toast && window.toast(e.message, 'error'));
    }
    function resetDismissed() {
      apiFetch('/api/analytics/alert-suggestions/dismissed', { method: 'DELETE' })
        .then(() => apiFetch('/api/analytics/alert-suggestions'))
        .then((r) => setSuggestions((r && r.suggestions) || []))
        .catch((e) => window.toast && window.toast(e.message, 'error'));
    }
    function createFromSuggestion(s) {
      /* Pre-fill the modal exactly the way startCreate(an) does — the
         AlertRuleModal treats falsy id as "new". */
      setCreateFrom({
        ...s.suggested_rule,
        site_id: null,
      });
    }

    /* Pre-fill the create-modal with sane defaults from the anomaly:
       metric=anomaly_zscore, comparator=gt, threshold=2.5, window=60. */
    function startCreate(an) {
      setCreateFrom({
        name: `Anomaly @ ${an.hour}`,
        metric: 'anomaly_zscore',
        comparator: 'gt',
        threshold: 2.5,
        window_minutes: 60,
        channel: 'inapp',
        channel_target: null,
        // not a real id — modal treats falsy id as "new"
      });
    }

    return (
      <div>
        {/* Suggested alerts — renders ABOVE the raw anomaly table because
            it's where the user finds the ready-to-apply value. */}
        {(suggestionsLoading || suggestions.length > 0 || suggestionsErr) && (
          <div style={{ marginBottom: 18 }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
              <div style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--aqos-text)' }}>
                ✦ Suggested alerts
                <span style={{ marginLeft: 8, fontWeight: 400, color: 'var(--aqos-text-faint)', fontSize: 11 }}>
                  Auto-detected from your recent activity
                </span>
              </div>
              <a onClick={resetDismissed} style={{ fontSize: 11, cursor: 'pointer', color: 'var(--aqos-text-dim)' }}>Reset dismissed</a>
            </div>
            {suggestionsLoading && (
              <div style={{ padding: 14, color: 'var(--aqos-text-faint)', fontSize: 12 }}>Looking for patterns…</div>
            )}
            {!suggestionsLoading && suggestions.length === 0 && !suggestionsErr && (
              <div style={{ padding: '14px 16px', fontSize: 12.5, background: 'var(--aqos-surface-2)', border: '1px solid var(--aqos-border)', borderRadius: 8, color: 'var(--aqos-text-dim)' }}>
                Nothing to suggest right now. Activity looks normal — we'll flag patterns as they emerge.
              </div>
            )}
            <div style={{ display: 'grid', gap: 10 }}>
              {suggestions.map((s) => <SuggestionCard key={s.key} s={s} canManage={canManage} onCreate={() => createFromSuggestion(s)} onDismiss={() => dismissSuggestion(s)} />)}
            </div>
            {suggestionsErr && (
              <div style={{ marginTop: 10, padding: '8px 12px', fontSize: 12, color: 'var(--aqos-danger)', background: 'color-mix(in srgb, var(--aqos-danger) 10%, transparent)', borderRadius: 6 }}>
                Couldn't load suggestions: {suggestionsErr}
              </div>
            )}
          </div>
        )}

        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          marginBottom: 12,
        }}>
          <div style={{ color: 'var(--aqos-text-dim)', fontSize: 12.5 }}>
            {loading ? 'Computing…' : `${anomalies.length} flagged hour${anomalies.length === 1 ? '' : 's'}`}
            {meta.baseline_count != null && (
              <span style={{ marginLeft: 8, color: 'var(--aqos-text-faint)', fontSize: 11 }}>
                · baseline = {meta.baseline_count} samples over 7d
              </span>
            )}
          </div>
        </div>

        <div className="x-analytics-panel" style={{ padding: 0 }}>
          <table className="x-analytics-table">
            <thead>
              <tr>
                <th>Hour</th>
                <th style={{ textAlign: 'right' }}>Observed</th>
                <th style={{ textAlign: 'right' }}>Mean (7d)</th>
                <th style={{ textAlign: 'right' }}>σ</th>
                <th style={{ textAlign: 'right' }}>z-score</th>
                {canManage && <th style={{ width: 130 }}></th>}
              </tr>
            </thead>
            <tbody>
              {!loading && anomalies.length === 0 && (
                <tr><td colSpan={canManage ? 6 : 5} style={{ padding: '36px 14px', textAlign: 'center', color: 'var(--aqos-text-faint)' }}>
                  Nothing unusual in the last 24 hours.
                </td></tr>
              )}
              {anomalies.map((an, i) => {
                const high = an.z > 0;
                return (
                  <tr key={an.hour + '_' + i}>
                    <td><strong>{an.hour}</strong></td>
                    <td style={{ textAlign: 'right' }}><strong>{_fmtN(an.observed)}</strong></td>
                    <td style={{ textAlign: 'right', color: 'var(--aqos-text-dim)' }}>{_fmtN(an.mean)}</td>
                    <td style={{ textAlign: 'right', color: 'var(--aqos-text-dim)' }}>{_fmtN(an.stddev)}</td>
                    <td style={{ textAlign: 'right' }}>
                      <span style={{
                        padding: '2px 6px', borderRadius: 4, fontSize: 11.5, fontWeight: 600,
                        background: high ? 'color-mix(in srgb, oklch(0.78 0.13 160) 18%, transparent)' : 'color-mix(in srgb, var(--aqos-danger) 18%, transparent)',
                        color: high ? 'oklch(0.78 0.13 160)' : 'var(--aqos-danger)',
                      }}>
                        {high ? '+' : ''}{an.z.toFixed(2)} σ
                      </span>
                    </td>
                    {canManage && (
                      <td style={{ textAlign: 'right' }}>
                        <a onClick={() => startCreate(an)} style={{ fontSize: 11, cursor: 'pointer' }}>+ Create alert</a>
                      </td>
                    )}
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>

        {createFrom && (
          <AlertRuleModal
            initial={createFrom}
            onCancel={() => setCreateFrom(null)}
            onSaved={() => setCreateFrom(null)}
          />
        )}

        {err && (
          <div style={{
            marginTop: 16, padding: '10px 14px', fontSize: 12.5,
            background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
            border: '1px solid color-mix(in srgb, var(--aqos-danger) 32%, transparent)',
            borderRadius: 8,
          }}>
            <strong>Couldn't load anomalies:</strong> {err}
          </div>
        )}
      </div>
    );
  }

  /* ─── Goals tab ──────────────────────────────────────────────── */

  const GOAL_PERIODS = [
    { id: 'day',     label: 'Daily' },
    { id: 'week',    label: 'Weekly' },
    { id: 'month',   label: 'Monthly' },
    { id: 'quarter', label: 'Quarterly' },
    { id: 'year',    label: 'Yearly' },
  ];

  function GoalsTab({ canManage }) {
    const [goals, setGoals] = useState([]);
    const [loading, setLoading] = useState(true);
    const [adding, setAdding] = useState(false);
    const [err, setErr] = useState(null);

    function reload() {
      setLoading(true);
      apiFetch('/api/analytics/goals')
        .then((r) => { setGoals((r && r.goals) || []); setLoading(false); })
        .catch((e) => { setErr(e.message); setLoading(false); });
    }
    useEffect(() => { reload(); }, []);

    function deleteGoal(g) {
      if (!window.confirm(`Delete goal "${g.name}"?`)) return;
      apiFetch('/api/analytics/goals/' + encodeURIComponent(g.id), { method: 'DELETE' })
        .then(reload).catch((e) => window.toast && window.toast(e.message, 'error'));
    }

    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
          <div style={{ color: 'var(--aqos-text-dim)', fontSize: 12.5 }}>
            {loading ? 'Loading…' : `${goals.length} goal${goals.length === 1 ? '' : 's'}`}
          </div>
          {canManage && <button className="x-btn" onClick={() => setAdding(true)}>+ New goal</button>}
        </div>

        {!loading && goals.length === 0 && (
          <div className="x-analytics-panel" style={{ padding: '36px 14px', textAlign: 'center', color: 'var(--aqos-text-faint)' }}>
            No goals yet. Set targets like "10K impressions/day" or "£500 revenue/week" to track progress at a glance.
            {canManage && <div style={{ marginTop: 10 }}><a onClick={() => setAdding(true)} style={{ cursor: 'pointer', color: 'var(--aqos-accent, oklch(0.66 0.16 270))' }}>Create your first goal →</a></div>}
          </div>
        )}

        <div style={{ display: 'grid', gap: 10 }}>
          {goals.map((g) => <GoalCard key={g.id} g={g} canManage={canManage} onDelete={() => deleteGoal(g)} />)}
        </div>

        {adding && (
          <GoalModal
            onCancel={() => setAdding(false)}
            onSaved={() => { setAdding(false); reload(); }}
          />
        )}

        {err && (
          <div style={{
            marginTop: 16, padding: '10px 14px', fontSize: 12.5,
            background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
            border: '1px solid color-mix(in srgb, var(--aqos-danger) 32%, transparent)',
            borderRadius: 8,
          }}>
            <strong>Couldn't load goals:</strong> {err}
          </div>
        )}
      </div>
    );
  }

  /* Single goal card with progress bar + projection. */
  function GoalCard({ g, canManage, onDelete }) {
    const m = metricMeta(g.metric);
    const onPace = g.on_pace;
    const tone = onPace ? 'oklch(0.78 0.13 160)' : 'oklch(0.78 0.14 75)';
    const progressClamped = Math.min(100, g.progress_pct);
    const overflow = g.progress_pct > 100;
    const fmt = (n) => {
      if (g.metric === 'revenue_cents') return '£' + ((Number(n) || 0) / 100).toLocaleString('en-GB', { maximumFractionDigits: 0 });
      if (g.metric === 'scan_rate') return (Number(n) || 0).toFixed(2) + '%';
      return (Number(n) || 0).toLocaleString('en-GB');
    };
    return (
      <div style={{
        padding: '14px 16px', background: 'var(--aqos-surface)',
        border: '1px solid var(--aqos-border)', borderRadius: 8,
      }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
          <div>
            <strong style={{ fontSize: 13 }}>{g.name}</strong>
            <span style={{ marginLeft: 8, fontSize: 11, color: 'var(--aqos-text-faint)' }}>
              {m.label} · {(GOAL_PERIODS.find((p) => p.id === g.period) || {}).label || g.period}
            </span>
          </div>
          {canManage && (
            <a onClick={onDelete} style={{ fontSize: 11, color: 'var(--aqos-danger)', cursor: 'pointer' }}>Delete</a>
          )}
        </div>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
          <span style={{ color: 'var(--aqos-text-dim)', fontSize: 12 }}>
            <strong style={{ color: 'var(--aqos-text)' }}>{fmt(g.observed)}</strong>
            <span style={{ color: 'var(--aqos-text-faint)' }}> / {fmt(g.target)}</span>
            <span style={{ marginLeft: 6, color: tone }}>{g.progress_pct}%</span>
          </span>
          <span style={{ fontSize: 11, color: 'var(--aqos-text-faint)' }}>
            {g.elapsed_pct}% of period elapsed · projected end {fmt(g.projection)} · {onPace ? <span style={{ color: 'oklch(0.78 0.13 160)' }}>on pace</span> : <span style={{ color: 'oklch(0.78 0.14 75)' }}>behind pace</span>}
          </span>
        </div>
        <div style={{ height: 8, background: 'var(--aqos-surface-2)', borderRadius: 4, position: 'relative', overflow: 'hidden' }}>
          {/* Progress bar */}
          <div style={{
            position: 'absolute', inset: 0, width: progressClamped + '%',
            background: tone, opacity: overflow ? 1 : 0.85,
            transition: 'width 200ms ease',
          }} />
          {/* Pace marker — vertical line at "where progress should be" */}
          <div style={{
            position: 'absolute', top: 0, bottom: 0,
            left: g.elapsed_pct + '%', width: 0,
            borderLeft: '1px dashed var(--aqos-text-faint)',
          }} title={`Expected pace: ${g.elapsed_pct}%`} />
        </div>
      </div>
    );
  }

  function GoalModal({ onCancel, onSaved }) {
    const [name, setName] = useState('');
    const [metric, setMetric] = useState('impressions');
    const [period, setPeriod] = useState('month');
    const [target, setTarget] = useState(10000);
    const [busy, setBusy] = useState(false);
    const [err, setErr] = useState(null);

    function submit(e) {
      if (e) e.preventDefault();
      if (!name.trim()) { setErr('Name required'); return; }
      if (!Number(target)) { setErr('Target must be > 0'); return; }
      setBusy(true); setErr(null);
      apiFetch('/api/analytics/goals', {
        method: 'POST',
        body: JSON.stringify({ name: name.trim(), metric, period, target: Number(target) }),
      })
        .then(() => { setBusy(false); onSaved(); })
        .catch((e) => { setBusy(false); setErr(e.message); });
    }

    const m = metricMeta(metric);

    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 200,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }} onClick={onCancel}>
        <form onSubmit={submit} onClick={(e) => e.stopPropagation()} style={{
          width: 460, background: 'var(--aqos-surface)', borderRadius: 10,
          border: '1px solid var(--aqos-border)', padding: 22,
          boxShadow: '0 20px 50px rgba(0,0,0,0.35)',
        }}>
          <h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 14 }}>New goal</h2>

          <Field label="Name">
            <input value={name} onChange={(e) => setName(e.target.value)}
                   placeholder="e.g. 10K impressions per day" style={inputStyle} autoFocus />
          </Field>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
            <Field label="Metric">
              <select value={metric} onChange={(e) => setMetric(e.target.value)} style={inputStyle}>
                {ALERT_METRICS.map((opt) => <option key={opt.id} value={opt.id}>{opt.label}</option>)}
              </select>
            </Field>
            <Field label="Period">
              <select value={period} onChange={(e) => setPeriod(e.target.value)} style={inputStyle}>
                {GOAL_PERIODS.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
              </select>
            </Field>
          </div>

          <Field label={`Target ${m.unit ? '(' + m.unit + ')' : ''}`}>
            <input type="number" value={target} onChange={(e) => setTarget(e.target.value)} style={inputStyle} />
            {metric === 'revenue_cents' && (
              <p style={{ fontSize: 11, color: 'var(--aqos-text-faint)', marginTop: 4 }}>
                Value is in pence — £100 = 10000.
              </p>
            )}
          </Field>

          {err && (
            <div style={{
              marginTop: 8, padding: '8px 12px', fontSize: 12.5,
              background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
              color: 'var(--aqos-danger)', borderRadius: 6,
            }}>{err}</div>
          )}

          <div style={{ marginTop: 14, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
            <button type="button" className="x-btn ghost" onClick={onCancel}>Cancel</button>
            <button type="submit" className="x-btn" disabled={busy || !name.trim()}>
              {busy ? 'Saving…' : 'Create goal'}
            </button>
          </div>
        </form>
      </div>
    );
  }

  /* ─── Scheduled reports tab ───────────────────────────────────── */

  const REPORT_RANGES = [
    { id: '24h', label: 'Last 24 hours' },
    { id: '7d',  label: 'Last 7 days' },
    { id: '30d', label: 'Last 30 days' },
    { id: '90d', label: 'Last 90 days' },
  ];
  const REPORT_CADENCES = [
    { id: 'daily',   label: 'Daily' },
    { id: 'weekly',  label: 'Weekly' },
    { id: 'monthly', label: 'Monthly' },
  ];
  const DOW_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

  function describeSchedule(cadence, schedule) {
    const s = Object.assign({ hour: 9, minute: 0, day_of_week: 1, day_of_month: 1 }, schedule || {});
    const time = String(s.hour).padStart(2, '0') + ':' + String(s.minute).padStart(2, '0') + ' UTC';
    if (cadence === 'daily')   return 'Daily · ' + time;
    if (cadence === 'weekly')  return `Weekly · ${DOW_LABELS[s.day_of_week]} ${time}`;
    if (cadence === 'monthly') return `Monthly · day ${s.day_of_month} at ${time}`;
    return 'Unknown';
  }

  function ReportsTab({ canManage }) {
    const [reports, setReports] = useState([]);
    const [loading, setLoading] = useState(true);
    const [editing, setEditing] = useState(null); // report or 'new'
    const [err, setErr] = useState(null);

    function reload() {
      setLoading(true);
      apiFetch('/api/analytics/scheduled-reports')
        .then((r) => { setReports((r && r.reports) || []); setLoading(false); })
        .catch((e) => { setErr(e.message); setLoading(false); });
    }
    useEffect(() => { reload(); }, []);

    function toggleStatus(rep) {
      const next = rep.status === 'active' ? 'paused' : 'active';
      apiFetch('/api/analytics/scheduled-reports/' + encodeURIComponent(rep.id), {
        method: 'PATCH', body: JSON.stringify({ status: next }),
      }).then(reload).catch((e) => window.toast && window.toast(e.message, 'error'));
    }
    function deleteReport(rep) {
      if (!window.confirm(`Delete scheduled report "${rep.name}"? Past run history is kept.`)) return;
      apiFetch('/api/analytics/scheduled-reports/' + encodeURIComponent(rep.id), { method: 'DELETE' })
        .then(reload).catch((e) => window.toast && window.toast(e.message, 'error'));
    }
    function cloneReport(rep) {
      apiFetch('/api/analytics/scheduled-reports/' + encodeURIComponent(rep.id) + '/clone', { method: 'POST' })
        .then(() => { window.toast && window.toast(`Cloned "${rep.name}" — opens paused`); reload(); })
        .catch((e) => window.toast && window.toast('Clone failed: ' + e.message, 'error'));
    }
    function runNow(rep) {
      window.toast && window.toast('Running "' + rep.name + '" now…');
      apiFetch('/api/analytics/scheduled-reports/' + encodeURIComponent(rep.id) + '/run', { method: 'POST' })
        .then((r) => {
          const msg = r.ok
            ? `Sent → ${r.recipients_count || 0} recipient${r.recipients_count === 1 ? '' : 's'}${r.info ? ' (' + r.info + ')' : ''}`
            : `Run failed: ${r.error || 'unknown'}`;
          window.toast && window.toast(msg, r.ok ? 'success' : 'error');
          reload();
        })
        .catch((e) => window.toast && window.toast('Run failed: ' + e.message, 'error'));
    }

    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
          <div style={{ color: 'var(--aqos-text-dim)', fontSize: 12.5 }}>
            {loading ? 'Loading…' : `${reports.length} scheduled report${reports.length === 1 ? '' : 's'}`}
          </div>
          {canManage && <button className="x-btn" onClick={() => setEditing('new')}>+ New scheduled report</button>}
        </div>

        <div className="x-analytics-panel" style={{ padding: 0 }}>
          <table className="x-analytics-table">
            <thead>
              <tr>
                <th style={{ width: 24 }}></th>
                <th>Name</th>
                <th>Range</th>
                <th>Cadence</th>
                <th>Recipients</th>
                <th style={{ textAlign: 'right' }}>Next run</th>
                {canManage && <th style={{ width: 180 }}></th>}
              </tr>
            </thead>
            <tbody>
              {!loading && reports.length === 0 && (
                <tr><td colSpan={canManage ? 7 : 6} style={{ padding: '36px 14px', textAlign: 'center', color: 'var(--aqos-text-faint)' }}>
                  No scheduled reports yet. {canManage ? <a onClick={() => setEditing('new')} style={{ cursor: 'pointer', color: 'var(--aqos-accent, oklch(0.66 0.16 270))' }}>Schedule one →</a> : null}
                </td></tr>
              )}
              {reports.map((r) => {
                const isActive = r.status === 'active';
                const recList = (r.recipients_email || '').split(',').map((s) => s.trim()).filter(Boolean);
                return (
                  <tr key={r.id}>
                    <td>
                      <span title={r.status} style={{
                        display: 'inline-block', width: 8, height: 8, borderRadius: 4,
                        background: isActive ? 'oklch(0.78 0.13 160)' : 'var(--aqos-text-faint)',
                      }} />
                    </td>
                    <td><strong>{r.name}</strong></td>
                    <td><span style={{ color: 'var(--aqos-text-dim)' }}>{(REPORT_RANGES.find((x) => x.id === r.range_id) || {}).label || r.range_id}</span></td>
                    <td><span style={{ color: 'var(--aqos-text-dim)' }}>{r.cadence_label || describeSchedule(r.cadence, r.schedule)}</span></td>
                    <td>
                      <span style={{ color: 'var(--aqos-text-dim)' }}>{recList.length} email{recList.length === 1 ? '' : 's'}</span>
                      {recList.length > 0 && (
                        <span style={{ marginLeft: 6, color: 'var(--aqos-text-faint)', fontSize: 11 }} title={recList.join(', ')}>
                          · {recList[0]}{recList.length > 1 ? ` (+${recList.length - 1})` : ''}
                        </span>
                      )}
                    </td>
                    <td style={{ textAlign: 'right', color: 'var(--aqos-text-faint)', fontSize: 11.5 }}>
                      {r.next_run_at ? new Date(r.next_run_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'}
                    </td>
                    {canManage && (
                      <td style={{ textAlign: 'right' }}>
                        <a onClick={() => cloneReport(r)} style={{ marginRight: 8, fontSize: 11, color: 'var(--aqos-text-dim)', cursor: 'pointer' }} title="Clone this report (paused)">Clone</a>
                        <a onClick={() => runNow(r)}     style={{ marginRight: 8, fontSize: 11, color: 'var(--aqos-text-dim)', cursor: 'pointer' }} title="Generate + send now">Run now</a>
                        <a onClick={() => toggleStatus(r)} style={{ marginRight: 8, fontSize: 11, color: 'var(--aqos-text-dim)', cursor: 'pointer' }}>
                          {isActive ? 'Pause' : 'Resume'}
                        </a>
                        <a onClick={() => setEditing(r)}  style={{ marginRight: 8, fontSize: 11, cursor: 'pointer' }}>Edit</a>
                        <a onClick={() => deleteReport(r)} style={{ fontSize: 11, color: 'var(--aqos-danger)', cursor: 'pointer' }}>Delete</a>
                      </td>
                    )}
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>

        {editing && (
          <ScheduledReportModal
            initial={editing === 'new' ? null : editing}
            onCancel={() => setEditing(null)}
            onSaved={() => { setEditing(null); reload(); }}
          />
        )}

        {err && (
          <div style={{
            marginTop: 16, padding: '10px 14px', fontSize: 12.5,
            background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
            border: '1px solid color-mix(in srgb, var(--aqos-danger) 32%, transparent)',
            borderRadius: 8,
          }}>
            <strong>Couldn't load reports:</strong> {err}
          </div>
        )}
      </div>
    );
  }

  /* Modal — author or edit a single scheduled report. Same shape /
     style as AlertRuleModal. The schedule sub-form swaps controls
     based on cadence (day-of-week chips for weekly, day-of-month
     dropdown for monthly, just hour/minute for daily). */
  function ScheduledReportModal({ initial, onCancel, onSaved }) {
    const isEdit = !!(initial && initial.id);
    const init = initial || {};
    const initSched = init.schedule || (init.schedule_json ? tryParse(init.schedule_json) : null) || { hour: 9, minute: 0, day_of_week: 1, day_of_month: 1 };

    const [name, setName]           = useState(init.name || '');
    const [rangeId, setRangeId]     = useState(init.range_id || '7d');
    const [cadence, setCadence]     = useState(init.cadence || 'weekly');
    const [hour, setHour]           = useState(initSched.hour ?? 9);
    const [minute, setMinute]       = useState(initSched.minute ?? 0);
    const [dow, setDow]             = useState(initSched.day_of_week ?? 1);
    const [dom, setDom]             = useState(initSched.day_of_month ?? 1);
    const [recipients, setRecipients] = useState(init.recipients_email || '');
    const [busy, setBusy] = useState(false);
    const [err, setErr] = useState(null);

    function tryParse(s) { try { return JSON.parse(s); } catch { return null; } }

    function submit(e) {
      if (e) e.preventDefault();
      if (!name.trim()) { setErr('Name required'); return; }
      if (!recipients.trim()) { setErr('At least one recipient email required'); return; }
      setBusy(true); setErr(null);

      const body = {
        name: name.trim(),
        range_id: rangeId,
        cadence,
        schedule: { hour: Number(hour) || 0, minute: Number(minute) || 0, day_of_week: Number(dow) || 0, day_of_month: Number(dom) || 1 },
        recipients_email: recipients.trim(),
      };
      const req = isEdit
        ? apiFetch('/api/analytics/scheduled-reports/' + encodeURIComponent(initial.id), { method: 'PATCH', body: JSON.stringify(body) })
        : apiFetch('/api/analytics/scheduled-reports', { method: 'POST', body: JSON.stringify(body) });
      req.then(() => { setBusy(false); onSaved(); })
         .catch((e) => { setBusy(false); setErr(e.message); });
    }

    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 200,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }} onClick={onCancel}>
        <form onSubmit={submit} onClick={(e) => e.stopPropagation()} style={{
          width: 540, maxHeight: '90vh', overflowY: 'auto',
          background: 'var(--aqos-surface)', borderRadius: 10,
          border: '1px solid var(--aqos-border)', padding: 22,
          boxShadow: '0 20px 50px rgba(0,0,0,0.35)',
        }}>
          <h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 4 }}>
            {isEdit ? 'Edit scheduled report' : 'New scheduled report'}
          </h2>
          <p style={{ fontSize: 12, color: 'var(--aqos-text-faint)', marginBottom: 16 }}>
            Generates the editorial analytics PDF on a schedule and emails it to the recipients you list.
          </p>

          <Field label="Name">
            <input value={name} onChange={(e) => setName(e.target.value)}
                   placeholder="e.g. Weekly board summary" style={inputStyle} autoFocus />
          </Field>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
            <Field label="Time range covered">
              <select value={rangeId} onChange={(e) => setRangeId(e.target.value)} style={inputStyle}>
                {REPORT_RANGES.map((r) => <option key={r.id} value={r.id}>{r.label}</option>)}
              </select>
            </Field>
            <Field label="Cadence">
              <select value={cadence} onChange={(e) => setCadence(e.target.value)} style={inputStyle}>
                {REPORT_CADENCES.map((c) => <option key={c.id} value={c.id}>{c.label}</option>)}
              </select>
            </Field>
          </div>

          <div style={{
            padding: 12, marginBottom: 12,
            background: 'var(--aqos-surface-2)', borderRadius: 6,
          }}>
            <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 8, textTransform: 'uppercase', letterSpacing: 0.5 }}>
              Schedule
            </div>
            {cadence === 'weekly' && (
              <div style={{ marginBottom: 10 }}>
                <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>Day of week</div>
                <div style={{ display: 'flex', gap: 4 }}>
                  {DOW_LABELS.map((label, i) => (
                    <span key={i} onClick={() => setDow(i)}
                          className={'x-analytics-chip' + (Number(dow) === i ? ' is-active' : '')}
                          style={{ cursor: 'pointer', flex: 1, textAlign: 'center' }}>
                      {label}
                    </span>
                  ))}
                </div>
              </div>
            )}
            {cadence === 'monthly' && (
              <div style={{ marginBottom: 10 }}>
                <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginBottom: 4 }}>Day of month (1–28)</div>
                <input type="number" min="1" max="28" value={dom} onChange={(e) => setDom(e.target.value)} style={inputStyle} />
              </div>
            )}
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
              <Field label="Hour (UTC)">
                <input type="number" min="0" max="23" value={hour} onChange={(e) => setHour(e.target.value)} style={inputStyle} />
              </Field>
              <Field label="Minute">
                <input type="number" min="0" max="59" value={minute} onChange={(e) => setMinute(e.target.value)} style={inputStyle} />
              </Field>
            </div>
          </div>

          <Field label="Recipients (comma-separated)">
            <input value={recipients} onChange={(e) => setRecipients(e.target.value)}
                   placeholder="ops@aquarium.com, board@aquarium.com" style={inputStyle} />
            <p style={{ marginTop: 4, fontSize: 11, color: 'var(--aqos-text-faint)' }}>
              Sent via SMTP — needs SMTP_HOST/SMTP_FROM env vars + nodemailer installed on the server. Otherwise runs are logged but not actually delivered.
            </p>
            {recipients.trim() && <SmtpTestButton to={recipients.split(',')[0].trim()} />}
          </Field>

          <div style={{
            marginTop: 14, padding: '10px 12px', fontSize: 12.5,
            background: 'var(--aqos-surface-2)', borderRadius: 6,
            color: 'var(--aqos-text-dim)',
          }}>
            <strong style={{ color: 'var(--aqos-text)' }}>Preview:</strong>{' '}
            Send the <em>{(REPORT_RANGES.find((r) => r.id === rangeId) || {}).label}</em> editorial PDF to{' '}
            <strong>{(recipients.split(',').filter((s) => s.trim()).length) || 0}</strong> recipient(s),
            <strong> {describeSchedule(cadence, { hour, minute, day_of_week: dow, day_of_month: dom }).toLowerCase()}</strong>.
          </div>

          {err && (
            <div style={{
              marginTop: 12, padding: '8px 12px', fontSize: 12.5,
              background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
              color: 'var(--aqos-danger)', borderRadius: 6,
            }}>{err}</div>
          )}

          <div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
            <button type="button" className="x-btn ghost" onClick={onCancel}>Cancel</button>
            <button type="submit" className="x-btn" disabled={busy || !name.trim() || !recipients.trim()}>
              {busy ? 'Saving…' : (isEdit ? 'Save changes' : 'Create scheduled report')}
            </button>
          </div>
        </form>
      </div>
    );
  }

  /* Expose the scheduled-report modal so the AnalyticsScreen ExportMenu
     can also open it. */
  window.ScheduledReportModal = ScheduledReportModal;

  /* ─── A/B experiments tab ────────────────────────────────────── */

  const EXPERIMENT_METRICS = [
    { id: 'hold_rate',  label: 'Hold rate (engagement)' },
    { id: 'scan_rate',  label: 'Scan rate (CTR)' },
    { id: 'impressions', label: 'Impressions' },
    { id: 'scans',       label: 'QR scans' },
    { id: 'cta_clicks',  label: 'CTA clicks' },
  ];

  /* Variant config knobs — the kiosk reads these via the public
     experiments-for-screen endpoint and applies them at render time.
     Each knob is a {key, label, type, options/min/max} spec; the
     editor below builds inputs from this list. New knobs added here
     show up in the modal automatically.

     Adding a new knob is a two-step deal:
       1. Add the spec here (frontend authoring)
       2. Read it on the kiosk side in display/index.html where the
          variant config is applied (currently the kiosk doesn't read
          variant configs yet — that's a follow-up integration). */
  const VARIANT_CONFIG_KNOBS = [
    {
      key: 'rotation_order',
      label: 'Species rotation order',
      type: 'select',
      options: [
        { id: '',           label: '(default — alphabetical)' },
        { id: 'popularity', label: 'By popularity (most-viewed first)' },
        { id: 'engagement', label: 'By engagement (highest hold rate first)' },
        { id: 'random',     label: 'Random shuffle each loop' },
        { id: 'recently_added', label: 'Newest species first' },
      ],
    },
    {
      key: 'hero_treatment',
      label: 'Hero treatment',
      type: 'select',
      options: [
        { id: '',        label: '(default — looping video)' },
        { id: 'video',   label: 'Looping video' },
        { id: 'static',  label: 'Static hero image' },
        { id: 'kenburns', label: 'Ken Burns pan' },
      ],
    },
    {
      key: 'rotation_seconds',
      label: 'Auto-rotation (seconds per species)',
      type: 'number',
      min: 5, max: 60,
      placeholder: 'default 12',
    },
    {
      key: 'sponsor_position',
      label: 'Sponsor logo position',
      type: 'select',
      options: [
        { id: '',         label: '(default — bottom-right)' },
        { id: 'bottom_right', label: 'Bottom right (corner card)' },
        { id: 'top_strip',    label: 'Top strip' },
        { id: 'bottom_strip', label: 'Bottom strip' },
        { id: 'hidden',       label: 'Hidden' },
      ],
    },
    {
      key: 'cta_prominence',
      label: 'CTA prominence',
      type: 'select',
      options: [
        { id: '',          label: '(default — subtle)' },
        { id: 'subtle',    label: 'Subtle (corner badge)' },
        { id: 'prominent', label: 'Prominent (full-width banner)' },
        { id: 'pulsing',   label: 'Pulsing animation' },
      ],
    },
    {
      key: 'show_qr',
      label: 'Show QR code',
      type: 'bool',
    },
  ];
  const EXPERIMENT_STATUS = {
    draft:   { label: 'Draft',   color: 'var(--aqos-text-faint)' },
    running: { label: 'Running', color: 'oklch(0.78 0.13 160)' },
    stopped: { label: 'Stopped', color: 'var(--aqos-text-dim)' },
  };

  function ExperimentsTab({ canManage }) {
    const [experiments, setExperiments] = useState([]);
    const [loading, setLoading] = useState(true);
    const [showCreate, setShowCreate] = useState(false);
    const [openId, setOpenId] = useState(null); // results panel
    const [err, setErr] = useState(null);

    function reload() {
      setLoading(true);
      apiFetch('/api/analytics/experiments')
        .then((r) => { setExperiments((r && r.experiments) || []); setLoading(false); })
        .catch((e) => { setErr(e.message); setLoading(false); });
    }
    useEffect(() => { reload(); }, []);

    function setStatus(exp, status) {
      apiFetch('/api/analytics/experiments/' + encodeURIComponent(exp.id), {
        method: 'PATCH', body: JSON.stringify({ status }),
      }).then(reload).catch((e) => window.toast && window.toast(e.message, 'error'));
    }
    function deleteExp(exp) {
      if (!window.confirm(`Delete experiment "${exp.name}"? Past assignments are wiped.`)) return;
      apiFetch('/api/analytics/experiments/' + encodeURIComponent(exp.id), { method: 'DELETE' })
        .then(reload).catch((e) => window.toast && window.toast(e.message, 'error'));
    }

    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
          <div style={{ color: 'var(--aqos-text-dim)', fontSize: 12.5 }}>
            {loading ? 'Loading…' : `${experiments.length} experiment${experiments.length === 1 ? '' : 's'}`}
            <span style={{ marginLeft: 8, fontSize: 11, color: 'var(--aqos-text-faint)' }}>
              Test variant configs across screens, see which lifts your engagement
            </span>
          </div>
          {canManage && <button className="x-btn" onClick={() => setShowCreate(true)}>+ New experiment</button>}
        </div>

        {!loading && experiments.length === 0 && (
          <div className="x-analytics-panel" style={{ padding: '36px 14px', textAlign: 'center', color: 'var(--aqos-text-faint)' }}>
            No experiments yet. Try variants of species rotation order, hero treatment, or sponsor placement to find what visitors engage with most.
            {canManage && <div style={{ marginTop: 10 }}><a onClick={() => setShowCreate(true)} style={{ cursor: 'pointer', color: 'var(--aqos-accent, oklch(0.66 0.16 270))' }}>Design your first experiment →</a></div>}
          </div>
        )}

        <div style={{ display: 'grid', gap: 10 }}>
          {experiments.map((exp) => {
            const statusMeta = EXPERIMENT_STATUS[exp.status] || EXPERIMENT_STATUS.draft;
            const isOpen = openId === exp.id;
            return (
              <div key={exp.id} style={{
                padding: '14px 16px', background: 'var(--aqos-surface)',
                border: '1px solid var(--aqos-border)', borderRadius: 8,
                borderLeft: `3px solid ${statusMeta.color}`,
              }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                  <div>
                    <strong style={{ fontSize: 13 }}>{exp.name}</strong>
                    <span style={{ marginLeft: 8, fontSize: 10.5, color: statusMeta.color, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.4 }}>● {statusMeta.label}</span>
                    <span style={{ marginLeft: 8, color: 'var(--aqos-text-faint)', fontSize: 11 }}>
                      · {exp.primary_metric} · {(exp.variants || []).length} variants
                    </span>
                    {exp.hypothesis && (
                      <div style={{ fontSize: 12, color: 'var(--aqos-text-dim)', marginTop: 4, fontStyle: 'italic' }}>
                        "{exp.hypothesis}"
                      </div>
                    )}
                  </div>
                  {canManage && (
                    <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                      {exp.status === 'draft' && <a onClick={() => setStatus(exp, 'running')} style={{ fontSize: 11, color: 'oklch(0.78 0.13 160)', cursor: 'pointer' }}>▶ Start</a>}
                      {exp.status === 'running' && <a onClick={() => setStatus(exp, 'stopped')} style={{ fontSize: 11, color: 'var(--aqos-text-dim)', cursor: 'pointer' }}>■ Stop</a>}
                      <a onClick={() => setOpenId(isOpen ? null : exp.id)} style={{ fontSize: 11, cursor: 'pointer' }}>{isOpen ? 'Hide' : 'Results'}</a>
                      <a onClick={() => deleteExp(exp)} style={{ fontSize: 11, color: 'var(--aqos-danger)', cursor: 'pointer' }}>Delete</a>
                    </div>
                  )}
                </div>
                {isOpen && <ExperimentResults experimentId={exp.id} variants={exp.variants || []} />}
              </div>
            );
          })}
        </div>

        {showCreate && (
          <ExperimentModal
            onCancel={() => setShowCreate(false)}
            onSaved={() => { setShowCreate(false); reload(); }}
          />
        )}

        {err && (
          <div style={{
            marginTop: 16, padding: '10px 14px', fontSize: 12.5,
            background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
            border: '1px solid color-mix(in srgb, var(--aqos-danger) 32%, transparent)',
            borderRadius: 8,
          }}>
            <strong>Couldn't load experiments:</strong> {err}
          </div>
        )}
      </div>
    );
  }

  function ExperimentResults({ experimentId }) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
      apiFetch('/api/analytics/experiments/' + encodeURIComponent(experimentId) + '/results')
        .then((r) => { setData(r); setLoading(false); })
        .catch(() => setLoading(false));
    }, [experimentId]);
    if (loading) return <div style={{ padding: '12px 0', color: 'var(--aqos-text-faint)', fontSize: 12 }}>Loading results…</div>;
    if (!data || !data.results || data.results.length === 0) return <div style={{ padding: '12px 0', color: 'var(--aqos-text-faint)', fontSize: 12 }}>No results yet.</div>;
    const metric = data.experiment.primary_metric;
    function metricValue(m) {
      if (metric === 'hold_rate') return _fmtPct(m.hold_rate_pct, 1);
      if (metric === 'scan_rate') return _fmtPct(m.scan_rate_pct, 2);
      return _fmtN(m[metric] || 0);
    }
    return (
      <div style={{ marginTop: 14, paddingTop: 14, borderTop: '1px solid var(--aqos-border)' }}>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 10 }}>
          {data.results.map((r, i) => {
            const sig = r.significance;
            const lift = r.lift_pct;
            const winning = lift != null && lift > 0 && sig && sig.significant;
            const losing = lift != null && lift < 0 && sig && sig.significant;
            return (
              <div key={r.variant.id} style={{
                padding: '10px 12px', background: 'var(--aqos-surface-2)', borderRadius: 6,
                border: winning ? '1px solid oklch(0.78 0.13 160)' : (losing ? '1px solid var(--aqos-danger)' : '1px solid transparent'),
              }}>
                <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--aqos-text-faint)', textTransform: 'uppercase', letterSpacing: 0.4 }}>
                  {r.variant.label} {r.is_control && <span>· control</span>}
                </div>
                <div style={{ fontSize: 22, fontWeight: 600, marginTop: 2 }}>
                  {metricValue(r.metrics)}
                </div>
                <div style={{ fontSize: 11, color: 'var(--aqos-text-dim)', marginTop: 2 }}>
                  {r.metrics.screens} screen{r.metrics.screens === 1 ? '' : 's'} · {_fmtN(r.metrics.exposures)} exposures
                </div>
                {!r.is_control && lift != null && (
                  <div style={{ marginTop: 4, fontSize: 11, color: lift > 0 ? 'oklch(0.78 0.13 160)' : 'var(--aqos-danger)' }}>
                    {lift > 0 ? '▲' : '▼'} {Math.abs(lift).toFixed(1)}% vs control
                    {sig && <span style={{ marginLeft: 6, color: sig.significant ? 'oklch(0.78 0.13 160)' : 'var(--aqos-text-faint)' }}>
                      · z={sig.z}, p={sig.p}{sig.significant ? ' ✓ significant' : ''}
                    </span>}
                    {sig === null && <span style={{ marginLeft: 6, color: 'var(--aqos-text-faint)' }}>· not enough data</span>}
                  </div>
                )}
              </div>
            );
          })}
        </div>
        <div style={{ marginTop: 8, fontSize: 11, color: 'var(--aqos-text-faint)' }}>
          Window: {String(data.window.from).slice(0, 10)} → {String(data.window.to).slice(0, 10)} ·
          Significance: two-proportion z-test, |z|&gt;1.96 ≈ 95% confidence
        </div>
      </div>
    );
  }

  /* VariantEditor — per-variant block in the experiment modal. Renders
     label + description + a knob picker that lets the author override
     individual config values without writing JSON. The control variant
     gets a hint that it represents "current behaviour" — no overrides
     means the kiosk runs its defaults. */
  function VariantEditor({ variant, isFirst, canRemove, onChange, onRemove }) {
    const config = variant.config || {};
    const [expanded, setExpanded] = useState(false);
    const knobCount = Object.keys(config).filter(k => config[k] !== '' && config[k] != null).length;

    function setKnob(key, value) {
      const next = { ...config };
      if (value === '' || value == null) delete next[key];
      else next[key] = value;
      onChange({ config: next });
    }

    return (
      <div style={{ marginBottom: 10, paddingBottom: 10, borderBottom: '1px solid var(--aqos-border)' }}>
        <div style={{ display: 'flex', gap: 6, marginBottom: 4, alignItems: 'center' }}>
          <input value={variant.label} onChange={(e) => onChange({ label: e.target.value })}
                 placeholder="Variant label" style={{ ...inputStyle, flex: 1 }} />
          {isFirst && (
            <span style={{ fontSize: 10, color: 'var(--aqos-text-faint)', textTransform: 'uppercase', letterSpacing: 0.4, padding: '4px 8px', background: 'var(--aqos-surface-2)', borderRadius: 3 }}>
              Control
            </span>
          )}
          {canRemove && (
            <a onClick={onRemove} style={{ fontSize: 11, color: 'var(--aqos-danger)', cursor: 'pointer' }}>✕</a>
          )}
        </div>
        <input value={variant.description || ''} onChange={(e) => onChange({ description: e.target.value })}
               placeholder="What this variant does (optional)" style={inputStyle} />

        <div style={{ marginTop: 6 }}>
          <a onClick={() => setExpanded(!expanded)} style={{ fontSize: 11, color: 'var(--aqos-text-dim)', cursor: 'pointer' }}>
            {expanded ? '▾' : '▸'} Variant config
            {knobCount > 0 && <span style={{ marginLeft: 6, color: 'oklch(0.66 0.16 270)', fontWeight: 600 }}>· {knobCount} override{knobCount === 1 ? '' : 's'}</span>}
            {isFirst && knobCount === 0 && <span style={{ marginLeft: 6, color: 'var(--aqos-text-faint)' }}>· current kiosk behaviour</span>}
          </a>
          {expanded && (
            <div style={{ marginTop: 8, padding: 10, background: 'var(--aqos-surface)', borderRadius: 4, border: '1px solid var(--aqos-border)' }}>
              {VARIANT_CONFIG_KNOBS.map((knob) => (
                <div key={knob.key} style={{ marginBottom: 8 }}>
                  <div style={{ fontSize: 10.5, color: 'var(--aqos-text-dim)', marginBottom: 3 }}>{knob.label}</div>
                  {knob.type === 'select' && (
                    <select
                      value={config[knob.key] || ''}
                      onChange={(e) => setKnob(knob.key, e.target.value)}
                      style={inputStyle}
                    >
                      {knob.options.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
                    </select>
                  )}
                  {knob.type === 'number' && (
                    <input
                      type="number"
                      min={knob.min} max={knob.max}
                      value={config[knob.key] || ''}
                      onChange={(e) => setKnob(knob.key, e.target.value === '' ? '' : Number(e.target.value))}
                      placeholder={knob.placeholder}
                      style={inputStyle}
                    />
                  )}
                  {knob.type === 'bool' && (
                    <select
                      value={config[knob.key] === true ? 'true' : config[knob.key] === false ? 'false' : ''}
                      onChange={(e) => {
                        const v = e.target.value;
                        setKnob(knob.key, v === '' ? '' : v === 'true');
                      }}
                      style={inputStyle}
                    >
                      <option value="">(default)</option>
                      <option value="true">Yes</option>
                      <option value="false">No</option>
                    </select>
                  )}
                </div>
              ))}
              <p style={{ fontSize: 10.5, color: 'var(--aqos-text-faint)', marginTop: 8 }}>
                Leave a knob blank to use the kiosk default. Variants with no overrides represent baseline behaviour.
              </p>
            </div>
          )}
        </div>
      </div>
    );
  }

  function ExperimentModal({ onCancel, onSaved }) {
    const [name, setName] = useState('');
    const [hypothesis, setHypothesis] = useState('');
    const [metric, setMetric] = useState('hold_rate');
    const [variants, setVariants] = useState([
      { label: 'Control', description: '', config: {} },
      { label: 'Variant A', description: '', config: {} },
    ]);
    const [busy, setBusy] = useState(false);
    const [err, setErr] = useState(null);

    function updateVariant(i, patch) {
      setVariants(variants.map((v, j) => i === j ? { ...v, ...patch } : v));
    }
    function addVariant() {
      setVariants([...variants, { label: `Variant ${String.fromCharCode(64 + variants.length)}`, description: '', config: {} }]);
    }
    function removeVariant(i) {
      if (variants.length <= 2) return;
      setVariants(variants.filter((_, j) => j !== i));
    }

    function submit(e) {
      if (e) e.preventDefault();
      if (!name.trim()) { setErr('Name required'); return; }
      if (variants.some(v => !v.label.trim())) { setErr('Every variant needs a label'); return; }
      setBusy(true); setErr(null);
      apiFetch('/api/analytics/experiments', {
        method: 'POST',
        body: JSON.stringify({
          name: name.trim(),
          hypothesis: hypothesis.trim() || null,
          primary_metric: metric,
          variants: variants.map(v => ({
            label: v.label.trim(),
            description: v.description.trim() || null,
            config: v.config || {},
          })),
        }),
      })
        .then(() => { setBusy(false); onSaved(); })
        .catch((e) => { setBusy(false); setErr(e.message); });
    }

    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 200,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }} onClick={onCancel}>
        <form onSubmit={submit} onClick={(e) => e.stopPropagation()} style={{
          width: 560, maxHeight: '90vh', overflowY: 'auto',
          background: 'var(--aqos-surface)', borderRadius: 10,
          border: '1px solid var(--aqos-border)', padding: 22,
          boxShadow: '0 20px 50px rgba(0,0,0,0.35)',
        }}>
          <h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 14 }}>New A/B experiment</h2>

          <Field label="Name">
            <input value={name} onChange={(e) => setName(e.target.value)}
                   placeholder="e.g. Sea-jelly hero video vs static" style={inputStyle} autoFocus />
          </Field>

          <Field label="Hypothesis (optional)">
            <input value={hypothesis} onChange={(e) => setHypothesis(e.target.value)}
                   placeholder="e.g. Looping hero video lifts hold rate by 20%+" style={inputStyle} />
          </Field>

          <Field label="Primary metric">
            <select value={metric} onChange={(e) => setMetric(e.target.value)} style={inputStyle}>
              {EXPERIMENT_METRICS.map(m => <option key={m.id} value={m.id}>{m.label}</option>)}
            </select>
            <p style={{ fontSize: 11, color: 'var(--aqos-text-faint)', marginTop: 4 }}>
              Determines the winner. Hold rate is best for engagement experiments; scan rate for CTR-style.
            </p>
          </Field>

          <div style={{
            padding: 12, marginBottom: 12,
            background: 'var(--aqos-surface-2)', borderRadius: 6,
          }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
              <span style={{ fontSize: 11, color: 'var(--aqos-text-dim)', textTransform: 'uppercase', letterSpacing: 0.4 }}>
                Variants ({variants.length})
              </span>
              <a onClick={addVariant} style={{ fontSize: 11, cursor: 'pointer', color: 'var(--aqos-text-dim)' }}>+ Add variant</a>
            </div>
            {variants.map((v, i) => (
              <VariantEditor
                key={i}
                variant={v}
                isFirst={i === 0}
                canRemove={variants.length > 2}
                onChange={(patch) => updateVariant(i, patch)}
                onRemove={() => removeVariant(i)}
              />
            ))}
            <p style={{ fontSize: 11, color: 'var(--aqos-text-faint)', marginTop: 6 }}>
              After saving, assign screens to variants via <code style={{ fontSize: 10.5 }}>POST /api/analytics/experiments/:id/assignments</code>
              and start the experiment from the list. The kiosk reads its variant config via the public assignments endpoint and applies it.
            </p>
          </div>

          {err && (
            <div style={{
              marginTop: 8, padding: '8px 12px', fontSize: 12.5,
              background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
              color: 'var(--aqos-danger)', borderRadius: 6,
            }}>{err}</div>
          )}

          <div style={{ marginTop: 14, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
            <button type="button" className="x-btn ghost" onClick={onCancel}>Cancel</button>
            <button type="submit" className="x-btn" disabled={busy || !name.trim()}>
              {busy ? 'Saving…' : 'Create experiment'}
            </button>
          </div>
        </form>
      </div>
    );
  }

  /* ─── Ingestion sources tab ──────────────────────────────────── */

  function IngestionTab({ canManage }) {
    const [sources, setSources] = useState([]);
    const [loading, setLoading] = useState(true);
    const [showCreate, setShowCreate] = useState(false);
    const [created, setCreated] = useState(null); // {id, ingest_url, secret} from POST
    const [err, setErr] = useState(null);

    function reload() {
      setLoading(true);
      apiFetch('/api/analytics/ingestion-sources')
        .then((r) => { setSources((r && r.sources) || []); setLoading(false); })
        .catch((e) => { setErr(e.message); setLoading(false); });
    }
    useEffect(() => { reload(); }, []);

    function deleteSource(s) {
      if (!window.confirm(`Delete ingestion source "${s.name}"? This breaks any external systems pointing at it.`)) return;
      apiFetch('/api/analytics/ingestion-sources/' + encodeURIComponent(s.id), { method: 'DELETE' })
        .then(reload).catch((e) => window.toast && window.toast(e.message, 'error'));
    }

    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
          <div style={{ color: 'var(--aqos-text-dim)', fontSize: 12.5 }}>
            {loading ? 'Loading…' : `${sources.length} source${sources.length === 1 ? '' : 's'}`}
            <span style={{ marginLeft: 8, fontSize: 11, color: 'var(--aqos-text-faint)' }}>
              External systems POST signed JSON → analytics events
            </span>
          </div>
          {canManage && <button className="x-btn" onClick={() => setShowCreate(true)}>+ New source</button>}
        </div>

        {!loading && sources.length === 0 && (
          <div className="x-analytics-panel" style={{ padding: '36px 14px', textAlign: 'center', color: 'var(--aqos-text-faint)' }}>
            No ingestion sources yet. Create one to receive events from Stripe webhooks, your POS, or any external system.
            {canManage && <div style={{ marginTop: 10 }}><a onClick={() => setShowCreate(true)} style={{ cursor: 'pointer', color: 'var(--aqos-accent, oklch(0.66 0.16 270))' }}>Create your first source →</a></div>}
          </div>
        )}

        <div className="x-analytics-panel" style={{ padding: 0 }}>
          {sources.length > 0 && (
            <table className="x-analytics-table">
              <thead>
                <tr>
                  <th>Name</th>
                  <th>Status</th>
                  <th style={{ textAlign: 'right' }}>Events</th>
                  <th style={{ textAlign: 'right' }}>Last event</th>
                  {canManage && <th style={{ width: 80 }}></th>}
                </tr>
              </thead>
              <tbody>
                {sources.map((s) => (
                  <tr key={s.id}>
                    <td>
                      <strong>{s.name}</strong>
                      {s.description && <div style={{ fontSize: 11, color: 'var(--aqos-text-faint)' }}>{s.description}</div>}
                      <div style={{ fontSize: 10.5, color: 'var(--aqos-text-faint)', fontFamily: 'var(--aq-ff-mono, ui-monospace, monospace)', marginTop: 2 }}>
                        POST /api/analytics/public/ingest/events/{s.id}
                      </div>
                    </td>
                    <td>
                      <span style={{
                        display: 'inline-block', width: 8, height: 8, borderRadius: 4, marginRight: 6,
                        background: s.status === 'active' ? 'oklch(0.78 0.13 160)' : 'var(--aqos-text-faint)',
                      }} />
                      {s.status}
                    </td>
                    <td className="x-analytics-table-num">{(s.event_count || 0).toLocaleString()}</td>
                    <td className="x-analytics-table-num" style={{ color: 'var(--aqos-text-faint)', fontSize: 11.5 }}>
                      {s.last_event_at ? new Date(s.last_event_at).toLocaleString() : 'never'}
                    </td>
                    {canManage && (
                      <td style={{ textAlign: 'right' }}>
                        <a onClick={() => deleteSource(s)} style={{ fontSize: 11, color: 'var(--aqos-danger)', cursor: 'pointer' }}>Delete</a>
                      </td>
                    )}
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>

        {showCreate && (
          <CreateIngestionModal
            onCancel={() => setShowCreate(false)}
            onCreated={(r) => { setCreated(r); setShowCreate(false); reload(); }}
          />
        )}
        {created && (
          <SecretRevealModal
            data={created}
            onClose={() => setCreated(null)}
          />
        )}

        {err && (
          <div style={{
            marginTop: 16, padding: '10px 14px', fontSize: 12.5,
            background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
            border: '1px solid color-mix(in srgb, var(--aqos-danger) 32%, transparent)',
            borderRadius: 8,
          }}>
            <strong>Couldn't load sources:</strong> {err}
          </div>
        )}
      </div>
    );
  }

  function CreateIngestionModal({ onCancel, onCreated }) {
    const [name, setName] = useState('');
    const [description, setDescription] = useState('');
    const [busy, setBusy] = useState(false);
    const [err, setErr] = useState(null);

    function submit(e) {
      if (e) e.preventDefault();
      if (!name.trim()) { setErr('Name required'); return; }
      setBusy(true); setErr(null);
      apiFetch('/api/analytics/ingestion-sources', {
        method: 'POST',
        body: JSON.stringify({ name: name.trim(), description: description.trim() || null }),
      })
        .then((r) => { setBusy(false); onCreated(r); })
        .catch((e) => { setBusy(false); setErr(e.message); });
    }

    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 200,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }} onClick={onCancel}>
        <form onSubmit={submit} onClick={(e) => e.stopPropagation()} style={{
          width: 460, background: 'var(--aqos-surface)', borderRadius: 10,
          border: '1px solid var(--aqos-border)', padding: 22,
          boxShadow: '0 20px 50px rgba(0,0,0,0.35)',
        }}>
          <h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 14 }}>New ingestion source</h2>

          <Field label="Name">
            <input value={name} onChange={(e) => setName(e.target.value)}
                   placeholder="e.g. Stripe webhooks · Square POS · Manual annotations"
                   style={inputStyle} autoFocus />
          </Field>

          <Field label="Description (optional)">
            <input value={description} onChange={(e) => setDescription(e.target.value)}
                   placeholder="What system will be sending events?"
                   style={inputStyle} />
          </Field>

          <div style={{
            marginTop: 8, padding: '10px 12px', borderRadius: 6,
            background: 'color-mix(in srgb, oklch(0.66 0.16 270) 10%, var(--aqos-surface-2))',
            color: 'var(--aqos-text-dim)', fontSize: 11.5,
          }}>
            <strong style={{ color: 'var(--aqos-text)' }}>Heads-up:</strong> the HMAC secret is shown once on the next screen. Copy it now — it cannot be recovered later. (Like a Stripe webhook signing secret.)
          </div>

          {err && (
            <div style={{
              marginTop: 12, padding: '8px 12px', fontSize: 12.5,
              background: 'color-mix(in srgb, var(--aqos-danger) 12%, transparent)',
              color: 'var(--aqos-danger)', borderRadius: 6,
            }}>{err}</div>
          )}

          <div style={{ marginTop: 14, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
            <button type="button" className="x-btn ghost" onClick={onCancel}>Cancel</button>
            <button type="submit" className="x-btn" disabled={busy || !name.trim()}>
              {busy ? 'Creating…' : 'Create source'}
            </button>
          </div>
        </form>
      </div>
    );
  }

  function SecretRevealModal({ data, onClose }) {
    function copy(text, label) {
      navigator.clipboard?.writeText(text).then(
        () => window.toast && window.toast(label + ' copied'),
        () => window.prompt('Copy this:', text)
      );
    }
    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 250,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>
        <div style={{
          width: 540, background: 'var(--aqos-surface)', borderRadius: 10,
          border: '1px solid var(--aqos-border)', padding: 22,
          boxShadow: '0 20px 50px rgba(0,0,0,0.45)',
        }}>
          <h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 6 }}>Source created — copy your secret now</h2>
          <p style={{ fontSize: 12.5, color: 'var(--aqos-text-dim)', marginBottom: 14 }}>
            <strong style={{ color: 'oklch(0.78 0.14 75)' }}>This is the only time you'll see this secret.</strong>{' '}
            Configure your external system with the URL + secret below. Sign each request body with HMAC-SHA256 using the secret and pass the hex digest as <code style={{ fontSize: 11.5 }}>X-AquaOS-Signature</code>.
          </p>

          <div style={{ fontSize: 11, color: 'var(--aqos-text-faint)', marginBottom: 4 }}>Ingest URL</div>
          <div style={{
            padding: '8px 10px', fontSize: 12, fontFamily: 'var(--aq-ff-mono, ui-monospace, monospace)',
            background: 'var(--aqos-surface-2)', borderRadius: 6,
            border: '1px solid var(--aqos-border)', wordBreak: 'break-all',
            marginBottom: 4,
          }}>{data.ingest_url}</div>
          <a onClick={() => copy(data.ingest_url, 'URL')} style={{ fontSize: 11, color: 'var(--aqos-text-dim)', cursor: 'pointer' }}>📋 Copy URL</a>

          <div style={{ fontSize: 11, color: 'var(--aqos-text-faint)', marginTop: 14, marginBottom: 4 }}>Secret (HMAC-SHA256 key)</div>
          <div style={{
            padding: '8px 10px', fontSize: 12, fontFamily: 'var(--aq-ff-mono, ui-monospace, monospace)',
            background: 'color-mix(in srgb, oklch(0.78 0.14 75) 10%, var(--aqos-surface-2))',
            border: '1px solid color-mix(in srgb, oklch(0.78 0.14 75) 30%, var(--aqos-border))',
            borderRadius: 6, wordBreak: 'break-all', marginBottom: 4,
          }}>{data.secret}</div>
          <a onClick={() => copy(data.secret, 'Secret')} style={{ fontSize: 11, color: 'var(--aqos-text-dim)', cursor: 'pointer' }}>📋 Copy secret</a>

          <details style={{ marginTop: 14, fontSize: 11.5, color: 'var(--aqos-text-dim)' }}>
            <summary style={{ cursor: 'pointer' }}>Example: signing a payload (Node.js)</summary>
            <pre style={{
              marginTop: 8, padding: '10px 12px', borderRadius: 6,
              background: 'var(--aqos-surface-2)', fontSize: 11,
              overflow: 'auto', fontFamily: 'var(--aq-ff-mono, ui-monospace, monospace)',
            }}>{`const crypto = require('crypto');
const secret = '${data.secret.slice(0, 8)}…';
const body = JSON.stringify({
  event_type: 'sale.attributed',
  screen_id: 'screen-uuid',
  data: { amount_cents: 1500 }
});
const sig = crypto.createHmac('sha256', Buffer.from(secret, 'hex'))
  .update(body, 'utf8').digest('hex');

await fetch('${data.ingest_url}', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json',
             'X-AquaOS-Signature': sig },
  body,
});`}</pre>
          </details>

          <div style={{ marginTop: 14, display: 'flex', justifyContent: 'flex-end' }}>
            <button type="button" className="x-btn" onClick={onClose}>I've saved the secret</button>
          </div>
        </div>
      </div>
    );
  }

  /* ─── Top-level screen ────────────────────────────────────────── */
  function AlertsScreen({ param }) {
    const canView   = Auth.canViewAnalyticsAlerts && Auth.canViewAnalyticsAlerts();
    const canManage = Auth.canManageAnalyticsAlerts && Auth.canManageAnalyticsAlerts();

    /* Tabs are simple — store the active tab in the hash so refreshes
       and deep-links survive. param looks like "rules" or "feed". */
    const [tab, setTab] = useState((param || 'rules').split('?')[0] || 'rules');

    function changeTab(id) {
      setTab(id);
      window.location.hash = '#alerts/' + id;
    }

    if (!canView) {
      return (
        <div className="aq-content">
          <div className="aq-content-inner" style={{ maxWidth: 720, padding: '60px 0' }}>
            <h1 style={{ fontSize: 22, fontWeight: 600, marginBottom: 8 }}>Alerts</h1>
            <p style={{ color: 'var(--aqos-text-dim)' }}>
              You don't have permission to view alerts. Ask your org admin to grant the
              <code> analytics.view_alerts</code> capability.
            </p>
          </div>
        </div>
      );
    }

    return (
      <div className="aq-content">
        <div className="aq-content-inner" style={{ maxWidth: 1400 }}>
          <header className="x-analytics-head" style={{ marginBottom: 14 }}>
            <div>
              <h1>Alerts</h1>
              <div className="x-analytics-meta">
                <span style={{ color: 'var(--aqos-text-faint)' }}>
                  Watch metrics, route fires to your team. Anomalies surface unusual hours automatically.
                </span>
              </div>
            </div>
          </header>

          <TabStrip
            tabs={[
              { id: 'rules',     label: 'Rules' },
              { id: 'feed',      label: 'Recent fires' },
              { id: 'anomalies', label: 'Anomalies' },
              { id: 'goals',     label: 'Goals' },
              { id: 'reports',   label: 'Scheduled reports' },
              { id: 'experiments', label: 'A/B tests' },
              { id: 'ingestion', label: 'Ingestion' },
            ]}
            active={tab}
            onSelect={changeTab}
          />

          {tab === 'rules'     && <RulesTab     canManage={canManage} />}
          {tab === 'feed'      && <FeedTab      canManage={canManage} />}
          {tab === 'anomalies' && <AnomaliesTab canManage={canManage} />}
          {tab === 'goals'     && <GoalsTab     canManage={canManage} />}
          {tab === 'reports'   && <ReportsTab   canManage={canManage} />}
          {tab === 'experiments' && <ExperimentsTab canManage={canManage} />}
          {tab === 'ingestion' && <IngestionTab canManage={canManage} />}
        </div>
      </div>
    );
  }

  window.AlertsScreen = AlertsScreen;
  /* Also expose the modal so future surfaces (e.g. an anomaly chip on
     the main analytics dashboard) can open it directly. */
  window.AlertRuleModal = AlertRuleModal;
})();
