// settings.jsx — Settings router + the five Workspace-scoped panes.
//
// R03 / T0007 (Phase 5). Five tabs:
//
//   Billing          — billing-link.jsx (own bill vs billed-via-parent)
//   Modules          — per-module trial state + Stripe Checkout / Portal
//   Members          — members-pane.jsx (memberships + invitations)
//   Cross-Workspace  — cross-ws-settings.jsx (sharing toggle + audit log)
//   Workspace        — display_name + send_as_email + products + ICPs
//
// All data paths go through `workspaces/{wid}/...`. The active workspace
// id is read from the URL (`/w/<wid>/settings/...`). Each pane is a
// sub-pane component that reads workspaceId via props.
//
// Exposes window.Settings.

(function (window) {
  'use strict';

  const {
    useAuth,
    signOut,
    db,
    useActiveWorkspaceId,
    useWorkspace,
    navigateTo,
  } = window.exoteamAuth || {};
  if (!useAuth) {
    console.error('settings.jsx: window.exoteamAuth missing');
    return;
  }

  // Base panes always rendered. The F021B "Open Questions" tab is
  // appended by `buildPanes()` only when the workspace doc has
  // unanswered open_questions[] — keeps the sidebar tidy when there is
  // nothing to do. The full id list (including 'open-questions') is in
  // ALL_PANE_IDS so URL parsing accepts it.
  // R07 (ADR 0003): web is admin chrome only. The 7 working-data
  // tabs (People / Customers / Competitors / Content / Decisions /
  // ICPs / Events) moved to the native app. Their JSX files orphan
  // here and get deleted in T0014.
  const BASE_PANES = [
    { id: 'billing', label: 'Billing' },
    { id: 'modules', label: 'Modules' },
    { id: 'members', label: 'Members' },
    { id: 'cross-ws', label: 'Cross-Workspace' },
    { id: 'workspace', label: 'Workspace' },
    { id: 'profile', label: 'Profile' },
  ];
  const ALL_PANE_IDS = new Set([...BASE_PANES.map((p) => p.id), 'open-questions']);

  function buildPanes(openQuestionsCount) {
    const panes = [...BASE_PANES];
    if (openQuestionsCount > 0) {
      const insertAfter = panes.findIndex((p) => p.id === 'workspace');
      const item = { id: 'open-questions', label: `Open Questions (${openQuestionsCount})` };
      panes.splice(insertAfter + 1, 0, item);
    }
    return panes;
  }

  function parsePaneId(workspaceId, pathname) {
    if (!workspaceId) return 'modules';
    const prefix = `/w/${workspaceId}/settings`;
    const path = pathname || window.location.pathname;
    if (!path.startsWith(prefix)) return 'modules';
    const tail = path.slice(prefix.length).replace(/^\/+/, '').replace(/\/+$/, '');
    if (!tail) return 'modules';
    return ALL_PANE_IDS.has(tail) ? tail : 'modules';
  }

  function Settings() {
    const { user, userDoc } = useAuth();
    const activeWorkspaceId = useActiveWorkspaceId();
    const { workspaces, loading: wsLoading } = useWorkspace();
    const [paneId, setPaneId] = React.useState(() =>
      parsePaneId(activeWorkspaceId),
    );
    // F021B — count of unanswered open_questions, driven by a live
    // workspace-doc snapshot. Powers both the conditional Open Questions
    // tab in the sidebar and the banner on the Workspace pane.
    const [openQuestionsCount, setOpenQuestionsCount] = React.useState(0);

    React.useEffect(() => {
      if (!activeWorkspaceId) {
        setOpenQuestionsCount(0);
        return undefined;
      }
      const unsub = db
        .collection('workspaces')
        .doc(activeWorkspaceId)
        .onSnapshot(
          (snap) => {
            const data = snap.exists ? snap.data() : null;
            const list = data && data.enrichment && data.enrichment.open_questions;
            setOpenQuestionsCount(Array.isArray(list) ? list.length : 0);
          },
          (err) => console.error('Settings: open_questions snapshot', err),
        );
      return unsub;
    }, [activeWorkspaceId]);

    React.useEffect(() => {
      const onPop = () => setPaneId(parsePaneId(activeWorkspaceId));
      window.addEventListener('popstate', onPop);
      return () => window.removeEventListener('popstate', onPop);
    }, [activeWorkspaceId]);

    React.useEffect(() => {
      setPaneId(parsePaneId(activeWorkspaceId));
    }, [activeWorkspaceId]);

    // No /w/ prefix → redirect to first workspace's settings.
    React.useEffect(() => {
      if (!user || wsLoading) return;
      if (!activeWorkspaceId && workspaces.length > 0) {
        navigateTo(`/w/${workspaces[0].workspace_id}/settings/modules`);
      }
    }, [user, wsLoading, activeWorkspaceId, workspaces]);

    // /w/<wid>/settings (no pane) → redirect to /modules.
    React.useEffect(() => {
      if (!activeWorkspaceId) return;
      const prefix = `/w/${activeWorkspaceId}/settings`;
      if (window.location.pathname === prefix) {
        window.history.replaceState({}, '', `${prefix}/modules`);
        setPaneId('modules');
      }
    }, [activeWorkspaceId]);

    function navigateToPane(id) {
      if (!activeWorkspaceId) return;
      navigateTo(`/w/${activeWorkspaceId}/settings/${id}`);
    }

    if (!activeWorkspaceId) {
      return (
        <div style={{ padding: 24 }}>
          <p className="exo-auth-hint">Loading workspace…</p>
        </div>
      );
    }

    return (
      <div className="exo-admin">
        <header className="exo-admin-header">
          <a
            href={`/w/${activeWorkspaceId}/dashboard`}
            className="exo-admin-header-logo"
            onClick={(e) => {
              e.preventDefault();
              navigateTo(`/w/${activeWorkspaceId}/dashboard`);
            }}
          >
            <img src="/lockup.svg" alt="exoteam" style={{ height: 22 }} />
          </a>
          {window.WorkspaceSwitcher && <window.WorkspaceSwitcher />}
          <nav className="exo-admin-nav">
            <a
              href={`/w/${activeWorkspaceId}/dashboard`}
              className="exo-admin-nav-item"
              onClick={(e) => {
                e.preventDefault();
                navigateTo(`/w/${activeWorkspaceId}/dashboard`);
              }}
            >
              Home
            </a>
            <a
              href={`/w/${activeWorkspaceId}/settings`}
              className="exo-admin-nav-item is-active"
            >
              Settings
            </a>
          </nav>
          <div className="exo-admin-header-right">
            {userDoc && userDoc.display_name && (
              <span className="exo-admin-header-user">{userDoc.display_name}</span>
            )}
            <button
              type="button"
              className="exo-admin-btn--link is-muted"
              onClick={() => signOut()}
            >
              Sign out
            </button>
          </div>
        </header>

        <div className="exo-admin-layout">
          <aside className="exo-admin-sidebar">
            {buildPanes(openQuestionsCount).map((p) => (
              <button
                key={p.id}
                type="button"
                className={
                  'exo-admin-sidebar-item ' +
                  (paneId === p.id ? 'is-active' : '')
                }
                onClick={() => navigateToPane(p.id)}
              >
                {p.label}
              </button>
            ))}
          </aside>
          <main className="exo-admin-pane">
            <Pane
              paneId={paneId}
              workspaceId={activeWorkspaceId}
              user={user}
              userDoc={userDoc}
              openQuestionsCount={openQuestionsCount}
              onNavigateToPane={navigateToPane}
            />
          </main>
        </div>
      </div>
    );
  }

  function Pane({ paneId, workspaceId, user, userDoc, openQuestionsCount, onNavigateToPane }) {
    switch (paneId) {
      case 'billing':
        return window.BillingLinkPane ? (
          <window.BillingLinkPane workspaceId={workspaceId} />
        ) : (
          <Loading label="Billing" />
        );
      case 'modules':
        return <ModulesPane workspaceId={workspaceId} />;
      case 'members':
        return window.MembersPane ? (
          <window.MembersPane workspaceId={workspaceId} />
        ) : (
          <Loading label="Members" />
        );
      case 'cross-ws':
        return window.CrossWsSettingsPane ? (
          <window.CrossWsSettingsPane workspaceId={workspaceId} />
        ) : (
          <Loading label="Cross-Workspace" />
        );
      case 'workspace':
        return (
          <WorkspacePane
            workspaceId={workspaceId}
            userDoc={userDoc}
            user={user}
            openQuestionsCount={openQuestionsCount}
            onNavigateToPane={onNavigateToPane}
          />
        );
      case 'profile':
        return window.ProfilePane ? (
          <window.ProfilePane workspaceId={workspaceId} user={user} userDoc={userDoc} />
        ) : (
          <Loading label="Profile" />
        );
      case 'open-questions':
        // F021B's tab is conditionally appended by buildPanes() when
        // the workspace doc has unanswered open_questions[]. Kept on
        // web because answering questions = recording a decision, an
        // admin-shaped act (no spine editing).
        return window.OpenQuestionsPane ? (
          <window.OpenQuestionsPane workspaceId={workspaceId} />
        ) : (
          <Loading label="Open Questions" />
        );
      default:
        return <Loading label="Settings" />;
    }
  }

  function Loading({ label }) {
    return (
      <div>
        <h2 className="exo-admin-title">{label}</h2>
        <p className="exo-admin-hint">Loading…</p>
      </div>
    );
  }

  // ---------- Modules pane (workspace-scoped) ----------
  function ModulesPane({ workspaceId }) {
    const catalog = (window.exoteamModules && window.exoteamModules.CATALOG) || [];
    const selectable =
      (window.exoteamModules && window.exoteamModules.selectableModules()) || [];
    const [modules, setModules] = React.useState({});
    const [loading, setLoading] = React.useState(true);
    const [busy, setBusy] = React.useState(null);
    const [workspace, setWorkspace] = React.useState(null);

    React.useEffect(() => {
      if (!workspaceId) return undefined;
      const unsub = db
        .collection('workspaces')
        .doc(workspaceId)
        .onSnapshot(
          (snap) => setWorkspace(snap.exists ? { id: snap.id, ...snap.data() } : null),
          (err) => console.error('ModulesPane: workspace snapshot', err),
        );
      return unsub;
    }, [workspaceId]);

    React.useEffect(() => {
      if (!workspaceId) return undefined;
      const unsub = db
        .collection('workspaces')
        .doc(workspaceId)
        .collection('modules')
        .onSnapshot(
          (snap) => {
            const next = {};
            snap.docs.forEach((d) => {
              next[d.id] = { id: d.id, ...d.data() };
            });
            setModules(next);
            setLoading(false);
          },
          () => setLoading(false),
        );
      return unsub;
    }, [workspaceId]);

    const billedViaParent = !!(workspace && workspace.billing_parent_id);

    async function toggle(moduleId, enable) {
      setBusy(moduleId);
      try {
        const callable = window.exoteamAuth.fns.httpsCallable(
          enable ? 'enableModule' : 'disableModule',
        );
        await callable({ workspace_id: workspaceId, module_id: moduleId });
      } catch (err) {
        alert((enable ? 'Enable' : 'Disable') + ' failed: ' + (err.message || err));
      } finally {
        setBusy(null);
      }
    }

    async function upgrade(moduleId) {
      setBusy(moduleId);
      try {
        const callable = window.exoteamAuth.fns.httpsCallable('createCheckoutSession');
        const { data } = await callable({
          workspace_id: workspaceId,
          module_id: moduleId,
          success_url:
            window.location.origin +
            `/w/${workspaceId}/settings/modules?upgraded=` +
            moduleId,
          cancel_url:
            window.location.origin + `/w/${workspaceId}/settings/modules`,
        });
        window.location.assign(data.checkout_url);
      } catch (err) {
        alert('Upgrade failed: ' + (err.message || err));
        setBusy(null);
      }
    }

    async function manage(moduleId) {
      setBusy(moduleId);
      try {
        const callable = window.exoteamAuth.fns.httpsCallable('createPortalSession');
        const { data } = await callable({
          workspace_id: workspaceId,
          return_url: window.location.origin + `/w/${workspaceId}/settings/modules`,
        });
        window.location.assign(data.portal_url);
      } catch (err) {
        if (err && err.code === 'failed-precondition') {
          alert('No active subscription yet — click "Upgrade" first.');
        } else {
          alert('Could not open Customer Portal: ' + (err.message || err));
        }
        setBusy(null);
      }
    }

    return (
      <div>
        <header className="exo-admin-pane-header">
          <div>
            <h2 className="exo-admin-title">Modules</h2>
            <p className="exo-admin-subtitle" style={{ margin: 0 }}>
              {billedViaParent
                ? 'This Workspace inherits its modules from its billing parent. Manage the subscription on the parent Workspace.'
                : 'Turn on the parts of exoteam you want. Each module starts a 14-day trial when first enabled.'}
            </p>
          </div>
        </header>

        {loading && <p className="exo-admin-hint">Loading…</p>}

        <div
          style={{
            display: 'grid',
            gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
            gap: 14,
            marginTop: 8,
            opacity: billedViaParent ? 0.6 : 1,
            pointerEvents: billedViaParent ? 'none' : 'auto',
          }}
        >
          {catalog
            .filter((m) => m.always_on)
            .map((m) => (
              <ModuleCard key={m.id} m={m} always_on />
            ))}
          {selectable.map((m) => {
            const doc = modules[m.id];
            const enabled = !!(doc && doc.enabled);
            return (
              <ModuleCard
                key={m.id}
                m={m}
                doc={doc}
                enabled={enabled}
                busy={busy === m.id}
                onToggle={(next) => toggle(m.id, next)}
                onUpgrade={() => upgrade(m.id)}
                onManage={() => manage(m.id)}
              />
            );
          })}
          {catalog
            .filter((m) => m.coming_soon)
            .map((m) => (
              <ModuleCard key={m.id} m={m} coming_soon />
            ))}
        </div>
      </div>
    );
  }

  function ModuleCard({ m, doc, enabled, busy, onToggle, onUpgrade, onManage, always_on, coming_soon }) {
    const trialEnds = doc && doc.trial_ends_at && doc.trial_ends_at.toDate
      ? doc.trial_ends_at.toDate()
      : null;
    const daysLeft = trialEnds
      ? Math.max(0, Math.ceil((trialEnds - Date.now()) / (24 * 60 * 60 * 1000)))
      : null;
    const subStatus = doc && doc.subscription && doc.subscription.status;
    const paid = subStatus === 'active' || subStatus === 'trialing';
    const accent = m.colour || 'var(--exo-ink)';
    return (
      <div
        className="exo-admin-card"
        style={{
          padding: 20,
          gap: 10,
          opacity: coming_soon ? 0.55 : 1,
        }}
      >
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <span style={{ width: 8, height: 8, borderRadius: 999, background: accent, flexShrink: 0 }} />
          <strong style={{ flex: 1, color: 'var(--exo-ink)' }}>{m.label}</strong>
          {always_on && <TinyPill text="always on" modifier="is-success" />}
          {coming_soon && <TinyPill text="coming soon" modifier="is-muted" />}
          {paid && <TinyPill text={subStatus} modifier="is-success" />}
          {enabled && !always_on && !paid && <TinyPill text="trial" modifier="is-success" />}
        </div>
        <p style={{ margin: 0, color: 'var(--exo-ink-secondary)', fontSize: 13.5, lineHeight: 1.5, minHeight: 40 }}>
          {m.blurb}
        </p>
        {enabled && !paid && daysLeft !== null && (
          <p style={{
            margin: 0,
            color: daysLeft > 2 ? 'var(--exo-ink-secondary)' : 'var(--exo-accent-destructive)',
            fontSize: 12.5,
          }}>
            {daysLeft > 0
              ? `Trial: ${daysLeft} day${daysLeft === 1 ? '' : 's'} left`
              : 'Trial ended'}
          </p>
        )}
        {!always_on && !coming_soon && (
          enabled ? (
            <div style={{ display: 'flex', gap: 14, marginTop: 4, alignItems: 'center' }}>
              {paid ? (
                <button
                  type="button"
                  className="exo-admin-btn--link"
                  disabled={busy}
                  onClick={onManage}
                  style={{ color: accent }}
                >
                  {busy ? '…' : 'Manage subscription'}
                </button>
              ) : (
                <>
                  <button
                    type="button"
                    className="exo-admin-btn--link"
                    disabled={busy}
                    onClick={onUpgrade}
                    style={{ color: accent, fontWeight: 600 }}
                  >
                    {busy
                      ? '…'
                      : m.price_gbp_monthly
                        ? `Upgrade — £${m.price_gbp_monthly}/mo`
                        : 'Upgrade'}
                  </button>
                  <button
                    type="button"
                    className="exo-admin-btn--link is-destructive"
                    disabled={busy}
                    onClick={() => onToggle(false)}
                  >
                    Disable
                  </button>
                </>
              )}
            </div>
          ) : (
            <button
              type="button"
              className="exo-admin-btn--link"
              disabled={busy}
              onClick={() => onToggle(true)}
              style={{ marginTop: 4, color: accent }}
            >
              {busy ? '…' : 'Enable'}
            </button>
          )
        )}
      </div>
    );
  }

  // Hairline-only pill — see `.exo-admin-pill` in site-shared.css. The
  // `modifier` arg picks the accent (is-success / is-warning / is-
  // destructive / is-muted / is-accent). Background stays transparent
  // per the explicit user directive on the new dialect.
  function TinyPill({ text, modifier }) {
    return (
      <span className={'exo-admin-pill ' + (modifier || 'is-muted')}>
        {text}
      </span>
    );
  }

  // ---------- Workspace pane: display_name + send_as + products + ICPs ----------
  function WorkspacePane({ workspaceId, userDoc, user, openQuestionsCount, onNavigateToPane }) {
    const [workspace, setWorkspace] = React.useState(null);
    const [displayName, setDisplayName] = React.useState('');
    const [sendAsEmail, setSendAsEmail] = React.useState('');
    const [saving, setSaving] = React.useState(false);
    const [saved, setSaved] = React.useState(false);
    const [error, setError] = React.useState(null);

    // F023B — enrichment re-run + diff panel state.
    const [showModal, setShowModal] = React.useState(false);
    const [enrichRunning, setEnrichRunning] = React.useState(false);
    const [enrichError, setEnrichError] = React.useState(null);
    // `diffSinceMs` is the workspace's `enrichment.last_enriched_at` at
    // the moment Re-run was clicked. Events with `ts > diffSinceMs`
    // belong to the just-finished run. Null means no diff panel is open.
    const [diffSinceMs, setDiffSinceMs] = React.useState(null);

    React.useEffect(() => {
      if (!workspaceId) return undefined;
      const unsub = db
        .collection('workspaces')
        .doc(workspaceId)
        .onSnapshot((snap) => {
          if (snap.exists) {
            const data = { id: snap.id, ...snap.data() };
            setWorkspace(data);
            setDisplayName(data.display_name || '');
            setSendAsEmail(data.send_as_email || '');
          }
        });
      return unsub;
    }, [workspaceId]);

    async function save() {
      setSaving(true);
      setError(null);
      try {
        await db
          .collection('workspaces')
          .doc(workspaceId)
          .set(
            {
              display_name: displayName.trim() || null,
              send_as_email: sendAsEmail.trim() || null,
            },
            { merge: true },
          );
        setSaved(true);
        setTimeout(() => setSaved(false), 1800);
      } catch (err) {
        console.error('WorkspacePane: save failed', err);
        setError(err.message || String(err));
      } finally {
        setSaving(false);
      }
    }

    async function confirmRerun() {
      if (!workspaceId || !workspace) return;
      // Capture the previous `last_enriched_at` BEFORE the call so the
      // diff panel can scope its events query to the new run only.
      const prevLastMs = _tsToMs((workspace.enrichment || {}).last_enriched_at);
      setShowModal(false);
      setEnrichRunning(true);
      setEnrichError(null);
      setDiffSinceMs(null);
      try {
        const callable = window.exoteamAuth.fns.httpsCallable('learnAboutWorkspace');
        await callable({
          workspace_id: workspaceId,
          website_url: workspace.website_url || null,
          linkedin_url: workspace.linkedin_url || null,
          crunchbase_url: workspace.crunchbase_url || null,
          github_url: workspace.github_url || null,
          twitter_url: workspace.twitter_url || null,
          companies_house_number: workspace.companies_house_number || null,
          // Deliberately NO extra_context — that's a wizard-specific
          // paste input the user supplies once during onboarding.
        });
        // The workspace doc snapshot will refresh with the new
        // last_enriched_at via the onSnapshot above; the diff panel
        // queries against `prevLastMs` (the value at click time).
        setDiffSinceMs(prevLastMs || 0);
      } catch (err) {
        console.error('learnAboutWorkspace failed', err);
        if (err && err.code === 'resource-exhausted') {
          setEnrichError(err.message || 'Re-run available again shortly.');
        } else {
          setEnrichError(err.message || String(err));
        }
      } finally {
        setEnrichRunning(false);
      }
    }

    if (!workspace) {
      return (
        <div>
          <h2 className="exo-admin-title">Workspace</h2>
          <p className="exo-admin-hint">Loading workspace…</p>
        </div>
      );
    }

    const enrichment = workspace.enrichment || {};
    const lastEnrichedAt = enrichment.last_enriched_at;
    const lastEnrichedMs = _tsToMs(lastEnrichedAt);
    const lastEnrichedLabel = lastEnrichedMs
      ? _relativeFromNow(lastEnrichedMs)
      : null;
    const lastConfidence = typeof enrichment.confidence === 'string' ? enrichment.confidence : null;
    const lastCitationCount = Array.isArray(enrichment.citations) ? enrichment.citations.length : 0;

    return (
      <div>
        <h2 className="exo-admin-title">Workspace</h2>
        <p className="exo-admin-subtitle">
          The display name + send-as email for this Workspace. Products
          and ICPs live alongside — manage them from the desktop client
          (the canonical store) which mirrors them here.
        </p>

        {openQuestionsCount > 0 && (
          <div className="exo-admin-banner is-trial" style={{ marginBottom: 18 }}>
            <span>
              <strong>
                {openQuestionsCount} open question{openQuestionsCount === 1 ? '' : 's'}
              </strong>
              {' '}need your input to sharpen the agent's context.
            </span>
            <button
              type="button"
              className="exo-admin-btn--link"
              onClick={() => onNavigateToPane && onNavigateToPane('open-questions')}
              style={{ fontWeight: 600, color: '#7a4c00' }}
            >
              Review →
            </button>
          </div>
        )}

        {error && (
          <div className="exo-admin-banner is-error" style={{ marginBottom: 14 }}>
            <span>{error}</span>
          </div>
        )}

        <label className="exo-admin-label" style={{ marginTop: 0 }}>Display name</label>
        <input
          className="exo-admin-input"
          value={displayName}
          onChange={(e) => setDisplayName(e.target.value)}
          placeholder="e.g. Untold Garden"
          disabled={saving}
        />

        <label className="exo-admin-label">
          Send-as email <span style={{ color: 'var(--exo-ink-tertiary)', fontWeight: 400 }}>(optional)</span>
        </label>
        <input
          className="exo-admin-input"
          type="email"
          value={sendAsEmail}
          onChange={(e) => setSendAsEmail(e.target.value)}
          placeholder="hi@yourbrand.com"
          disabled={saving}
        />
        <p className="exo-admin-hint">
          Drafts from this Workspace will send from this address. Must be
          configured as a Gmail send-as alias on the connected Google
          Workspace.
        </p>

        <button
          type="button"
          className="exo-admin-btn"
          onClick={save}
          disabled={saving}
          style={{ marginTop: 18, alignSelf: 'flex-start' }}
        >
          {saving ? 'Saving…' : saved ? 'Saved ✓' : 'Save'}
        </button>

        <hr className="exo-admin-divider" />

        <h3 className="exo-admin-section-title">Enrichment</h3>
        <p className="exo-admin-subtitle" style={{ marginTop: 0, marginBottom: 8 }}>
          {lastEnrichedLabel
            ? <>Last enriched: <strong>{lastEnrichedLabel}</strong>.</>
            : <>Never — run enrichment to populate your spine.</>}
        </p>
        {lastEnrichedLabel && lastConfidence && (
          <p className="exo-admin-hint" style={{ marginTop: 0 }}>
            Last run confidence: {lastConfidence} · {lastCitationCount} source{lastCitationCount === 1 ? '' : 's'}
          </p>
        )}
        {enrichError && (
          <div className="exo-admin-banner is-error" style={{ marginTop: 10 }}>
            <span>{enrichError}</span>
          </div>
        )}
        <button
          type="button"
          className="exo-admin-btn"
          onClick={() => setShowModal(true)}
          disabled={enrichRunning}
          style={{ marginTop: 14, alignSelf: 'flex-start' }}
        >
          {enrichRunning ? 'Running…' : 'Re-run enrichment'}
        </button>
        {enrichRunning && (
          <p className="exo-admin-hint">
            Reading + searching the web…
          </p>
        )}

        {showModal && (
          <ConfirmEnrichmentModal
            displayName={workspace.display_name || 'this Workspace'}
            onCancel={() => setShowModal(false)}
            onConfirm={confirmRerun}
          />
        )}

        {diffSinceMs !== null && !enrichRunning && (
          <EnrichmentDiffPanel
            workspaceId={workspaceId}
            sinceMs={diffSinceMs}
            onDismiss={() => setDiffSinceMs(null)}
          />
        )}

        <hr className="exo-admin-divider" />

        <InsightsSection workspace={workspace} workspaceId={workspaceId} />

        <hr className="exo-admin-divider" />

        <h3 className="exo-admin-section-title">Account</h3>
        <p className="exo-admin-subtitle" style={{ marginBottom: 6 }}>
          Signed in as <strong>{user && user.email}</strong>.
        </p>
        <p className="exo-admin-subtitle">
          Need to delete your account?{' '}
          <a
            href="mailto:hello@exoteam.ai?subject=Delete%20my%20exoteam%20account"
            style={{ color: 'var(--exo-accent)' }}
          >
            Email us
          </a>{' '}
          — self-serve deletion lands in a follow-up.
        </p>
      </div>
    );
  }

  // ---------- F020B: Insights subsection ----------
  //
  // Shows the four insight surfaces materialised by the desktop sidecar:
  //   * Voice fingerprint scalars (formality, avg length, em-dash rate).
  //   * Stakeholder count (computed from people/* docs with
  //     `is_stakeholder: true`).
  //   * Topic-cluster count (content/* with `medium: 'topic-cluster'`).
  // The "Refresh insights now" button calls `requestInsightsRefresh`
  // which flips the marker the sidecar polls.
  function InsightsSection({ workspace, workspaceId }) {
    const [stakeholderCount, setStakeholderCount] = React.useState(null);
    const [clusterCount, setClusterCount] = React.useState(null);
    const [requesting, setRequesting] = React.useState(false);
    const [requestError, setRequestError] = React.useState(null);
    const [requestSuccess, setRequestSuccess] = React.useState(false);

    React.useEffect(() => {
      if (!workspaceId) return undefined;
      // people/* count with is_stakeholder filter
      const unsubPeople = db
        .collection('workspaces')
        .doc(workspaceId)
        .collection('people')
        .where('is_stakeholder', '==', true)
        .onSnapshot(
          (snap) => setStakeholderCount(snap.size),
          () => setStakeholderCount(null),
        );
      const unsubContent = db
        .collection('workspaces')
        .doc(workspaceId)
        .collection('content')
        .where('medium', '==', 'topic-cluster')
        .onSnapshot(
          (snap) => setClusterCount(snap.size),
          () => setClusterCount(null),
        );
      return () => {
        unsubPeople();
        unsubContent();
      };
    }, [workspaceId]);

    const vf = (workspace && workspace.voice_fingerprint) || null;
    const lastRefreshed = workspace && workspace.insights_last_refreshed_at;
    const lastRefreshedMs = _tsToMs(lastRefreshed);
    const lastRefreshedLabel = lastRefreshedMs
      ? _relativeFromNow(lastRefreshedMs)
      : null;
    const requestedAtMs = _tsToMs(
      workspace && workspace.insights_refresh_requested_at,
    );

    async function requestRefresh() {
      if (!workspaceId) return;
      setRequesting(true);
      setRequestError(null);
      setRequestSuccess(false);
      try {
        const callable = window.exoteamAuth.fns.httpsCallable(
          'requestInsightsRefresh',
        );
        await callable({ workspace_id: workspaceId });
        setRequestSuccess(true);
        setTimeout(() => setRequestSuccess(false), 2400);
      } catch (err) {
        console.error('requestInsightsRefresh failed', err);
        setRequestError(err.message || String(err));
      } finally {
        setRequesting(false);
      }
    }

    return (
      <div>
        <h3 className="exo-admin-section-title">Insights</h3>
        <p className="exo-admin-subtitle" style={{ marginTop: 0, marginBottom: 6 }}>
          {lastRefreshedLabel
            ? <>Last refreshed: <strong>{lastRefreshedLabel}</strong>.</>
            : <>Never — the paired desktop refreshes insights weekly.</>}
        </p>
        <p className="exo-admin-hint" style={{ marginTop: 0 }}>
          Your Gmail + Calendar are read locally on your paired Mac. Only
          summaries — names, frequencies, inferred roles — are uploaded.
          Email bodies and meeting details never leave your device.
        </p>

        {vf && (
          <div
            className="exo-admin-card"
            style={{ padding: '16px 18px', marginTop: 12 }}
          >
            <div style={{ fontWeight: 600, marginBottom: 6, color: 'var(--exo-ink)' }}>
              Voice fingerprint
            </div>
            {vf.qualitative_summary && (
              <div style={{ fontSize: 13.5, marginBottom: 8, color: 'var(--exo-ink-secondary)', lineHeight: 1.5 }}>
                {vf.qualitative_summary}
              </div>
            )}
            <div style={{ fontSize: 12.5, color: 'var(--exo-ink-tertiary)' }}>
              {typeof vf.avg_email_length === 'number' && (
                <>~{vf.avg_email_length} words median</>
              )}
              {typeof vf.em_dash_rate === 'number' && (
                <> · {vf.em_dash_rate.toFixed(1)} em-dashes/msg</>
              )}
              {typeof vf.british_english_score === 'number' && (
                <> · {vf.british_english_score > 0.6 ? 'British English' : vf.british_english_score < 0.4 ? 'American English' : 'mixed spelling'}</>
              )}
              {typeof vf.formality_score === 'number' && (
                <> · formality {Math.round(vf.formality_score * 100)}%</>
              )}
            </div>
          </div>
        )}

        <div style={{
          display: 'flex',
          gap: 18,
          marginTop: 12,
          fontSize: 13.5,
          color: 'var(--exo-ink-secondary)',
        }}>
          <div>
            <strong>{stakeholderCount === null ? '—' : stakeholderCount}</strong> stakeholders
          </div>
          <div>
            <strong>{clusterCount === null ? '—' : clusterCount}</strong> topic clusters
          </div>
        </div>

        {requestError && (
          <div className="exo-admin-banner is-error" style={{ marginTop: 10 }}>
            <span>{requestError}</span>
          </div>
        )}

        <button
          type="button"
          className="exo-admin-btn"
          onClick={requestRefresh}
          disabled={requesting}
          style={{ marginTop: 14, alignSelf: 'flex-start' }}
        >
          {requesting
            ? 'Requesting…'
            : requestSuccess
              ? 'Requested ✓'
              : 'Refresh insights'}
        </button>
        {requestedAtMs && requestedAtMs > (lastRefreshedMs || 0) && (
          <p className="exo-admin-hint">
            Refresh requested — the paired desktop will run it on the next
            poll cycle.
          </p>
        )}
      </div>
    );
  }

  // ---------- F023B: Re-run enrichment confirmation modal ----------
  function ConfirmEnrichmentModal({ displayName, onCancel, onConfirm }) {
    // Simple in-component overlay (no modal library). Backdrop is a
    // fixed-position semi-opaque layer; the card centres above it.
    React.useEffect(() => {
      const onKey = (e) => { if (e.key === 'Escape') onCancel(); };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [onCancel]);
    return (
      <div className="exo-admin-modal-backdrop" onClick={onCancel}>
        <div className="exo-admin-modal" onClick={(e) => e.stopPropagation()}>
          <h3 className="exo-admin-modal-title">Re-run enrichment?</h3>
          <p className="exo-admin-modal-body" style={{ marginBottom: 6 }}>
            Refreshes the spine entities for <strong>{displayName}</strong> using the latest web data. Entities you have manually edited (those you've saved a body for) are preserved.
          </p>
          <p className="exo-admin-modal-body">
            Each run uses a Gemini call (a few pence) and takes ~15-30s. You can re-run at most once per hour.
          </p>
          <div className="exo-admin-modal-actions">
            <button
              type="button"
              className="exo-admin-btn exo-admin-btn--ghost"
              onClick={onCancel}
            >
              Cancel
            </button>
            <button
              type="button"
              className="exo-admin-btn"
              onClick={onConfirm}
            >
              Run enrichment
            </button>
          </div>
        </div>
      </div>
    );
  }

  // ---------- F023B: Diff panel — summarises events in the latest run ----------
  //
  // Reads `workspaces/{wid}/events` where `ts > sinceMs`, ordered ascending.
  // The events live in the same workspace doc the wizard / Settings UI
  // already subscribes to, so no rules changes are needed.
  //
  // Click-through: each entity row navigates to `/w/<wid>/settings/<kind>`
  // with a `#slug=<slug>` hash. spine-tabs.jsx reads that hash to pre-
  // select the matching entity in the F024 detail panel. (Chose URL
  // hash over local state so the navigation survives the route change
  // and the user can copy / share the link.)
  function EnrichmentDiffPanel({ workspaceId, sinceMs, onDismiss }) {
    const [events, setEvents] = React.useState(null); // null = loading
    const [error, setError] = React.useState(null);
    const [preservedRows, setPreservedRows] = React.useState([]);
    const [lowConfidenceRows, setLowConfidenceRows] = React.useState([]);

    // Load events with ts > sinceMs. Cap at 50 — one run emits ≤ ~13
    // events (1 enrichment.run + ≤ 4 kinds × N entities); 50 is plenty
    // of headroom + a safety stop.
    React.useEffect(() => {
      if (!workspaceId) return;
      let cancelled = false;
      const sinceDate = new Date(sinceMs || 0);
      db.collection('workspaces')
        .doc(workspaceId)
        .collection('events')
        .where('ts', '>', sinceDate)
        .orderBy('ts', 'asc')
        .limit(50)
        .get()
        .then(async (snap) => {
          if (cancelled) return;
          const rows = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
          setEvents(rows);
          // Resolve preserved + low-confidence entities by reading each
          // referenced spine doc. Preserved = updated by the agent but
          // body_md untouched (because `last_edited_by_user_at` is set
          // and changed_keys excludes body_md).
          const entityEvents = rows.filter(
            (r) => (r.kind === 'entity.created' || r.kind === 'entity.updated') && r.entity_path,
          );
          const preserved = [];
          const lowConf = [];
          await Promise.all(entityEvents.map(async (ev) => {
            const path = ev.entity_path;
            const slash = path.indexOf('/');
            if (slash <= 0) return;
            const kind = path.slice(0, slash);
            const slug = path.slice(slash + 1);
            try {
              const docSnap = await db.collection('workspaces').doc(workspaceId)
                .collection(kind).doc(slug).get();
              if (!docSnap.exists) return;
              const data = docSnap.data() || {};
              const changedKeys = (ev.payload && ev.payload.changed_keys) || [];
              if (
                ev.kind === 'entity.updated'
                && data.last_edited_by_user_at
                && !changedKeys.includes('body_md')
              ) {
                preserved.push({ kind, slug, display_name: data.display_name || slug });
              }
              if (data.confidence === 'low') {
                lowConf.push({ kind, slug, display_name: data.display_name || slug });
              }
            } catch (err) {
              console.warn('EnrichmentDiffPanel: spine doc read failed', { path, err });
            }
          }));
          if (cancelled) return;
          setPreservedRows(preserved);
          setLowConfidenceRows(lowConf);
        })
        .catch((err) => {
          if (cancelled) return;
          console.error('EnrichmentDiffPanel: events query failed', err);
          setError(err.message || String(err));
        });
      return () => { cancelled = true; };
    }, [workspaceId, sinceMs]);

    function navigateToEntity(kind, slug) {
      navigateTo(`/w/${workspaceId}/settings/${kind}#slug=${encodeURIComponent(slug)}`);
    }

    if (error) {
      return (
        <div className="exo-admin-card" style={{ marginTop: 14, padding: 18 }}>
          <p style={{ margin: 0, color: 'var(--exo-accent-destructive)' }}>
            Couldn't load diff: {error}
          </p>
        </div>
      );
    }
    if (events === null) {
      return (
        <div className="exo-admin-card" style={{ marginTop: 14, padding: 18 }}>
          <p className="exo-admin-hint" style={{ margin: 0 }}>Loading diff…</p>
        </div>
      );
    }

    const runEvent = events.find((e) => e.kind === 'enrichment.run');
    const runPayload = (runEvent && runEvent.payload) || {};
    const entityEvents = events.filter(
      (e) => e.kind === 'entity.created' || e.kind === 'entity.updated',
    );
    const created = entityEvents.filter((e) => e.kind === 'entity.created');
    const updated = entityEvents.filter((e) => e.kind === 'entity.updated');

    // Compose headline counts. Prefer the enrichment.run payload (it
    // pre-aggregates per kind in the same write) but fall back to
    // walking entity events if the run event hasn't materialised yet
    // (server-stamped `ts` race — should be rare).
    const headlines = [];
    function _push(label, n) { if (n > 0) headlines.push(`${n} ${label}${n === 1 ? '' : 's'}`); }
    const fromRun = runPayload && (runPayload.products_added !== undefined);
    if (fromRun) {
      _push('product', (runPayload.products_added || 0));
      _push('customer', (runPayload.customers_added || 0));
      _push('competitor', (runPayload.competitors_added || 0));
      _push('person', (runPayload.people_added || 0));
    }
    const updatedHeadlines = [];
    if (fromRun) {
      _pushTo(updatedHeadlines, 'product', (runPayload.products_updated || 0));
      _pushTo(updatedHeadlines, 'customer', (runPayload.customers_updated || 0));
      _pushTo(updatedHeadlines, 'competitor', (runPayload.competitors_updated || 0));
      _pushTo(updatedHeadlines, 'person', (runPayload.people_updated || 0));
    }
    const totalChanges = created.length + updated.length;

    return (
      <div className="exo-admin-card" style={{ marginTop: 14, padding: 20 }}>
        <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
          <div style={{ flex: 1 }}>
            <strong style={{ fontSize: 15, color: 'var(--exo-ink)' }}>
              Enrichment run finished — {totalChanges} change{totalChanges === 1 ? '' : 's'}
            </strong>
            {headlines.length > 0 && (
              <p style={{ margin: '6px 0 0', color: 'var(--exo-ink-secondary)', fontSize: 13 }}>
                Added: {headlines.join(' · ')}
              </p>
            )}
            {updatedHeadlines.length > 0 && (
              <p style={{ margin: '4px 0 0', color: 'var(--exo-ink-secondary)', fontSize: 13 }}>
                Updated: {updatedHeadlines.join(' · ')}
              </p>
            )}
            {preservedRows.length > 0 && (
              <p style={{ margin: '4px 0 0', color: 'var(--exo-ink-secondary)', fontSize: 13 }}>
                Preserved: {preservedRows.length} user-edited doc{preservedRows.length === 1 ? '' : 's'}
                {' '}({preservedRows.map((r) => r.display_name).join(', ')})
              </p>
            )}
          </div>
          <button
            type="button"
            className="exo-admin-btn--link is-muted"
            onClick={onDismiss}
            aria-label="Dismiss"
          >
            ✕ Dismiss
          </button>
        </div>

        {(created.length > 0 || updated.length > 0) && (
          <div style={{ marginTop: 14 }}>
            <h4 className="exo-admin-eyebrow" style={{ margin: '0 0 8px' }}>Itemised changes</h4>
            <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 4 }}>
              {[...created, ...updated].map((ev) => {
                const slash = (ev.entity_path || '').indexOf('/');
                if (slash <= 0) return null;
                const kind = ev.entity_path.slice(0, slash);
                const slug = ev.entity_path.slice(slash + 1);
                return (
                  <li key={ev.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
                    <button
                      type="button"
                      className="exo-admin-btn--link"
                      onClick={() => navigateToEntity(kind, slug)}
                      style={{ textAlign: 'left', flex: 1 }}
                    >
                      {ev.kind === 'entity.created' ? '+ ' : '· '}
                      {kind} / {slug}
                    </button>
                  </li>
                );
              })}
            </ul>
          </div>
        )}

        {lowConfidenceRows.length > 0 && (
          <div style={{ marginTop: 14 }}>
            <h4 className="exo-admin-eyebrow" style={{ margin: '0 0 8px' }}>Worth a second look (low confidence)</h4>
            <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 4 }}>
              {lowConfidenceRows.map((r) => (
                <li key={`${r.kind}/${r.slug}`} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
                  <button
                    type="button"
                    className="exo-admin-btn--link"
                    onClick={() => navigateToEntity(r.kind, r.slug)}
                    style={{ textAlign: 'left', flex: 1 }}
                  >
                    {r.kind}: {r.display_name} — Review individually →
                  </button>
                </li>
              ))}
            </ul>
          </div>
        )}

        {totalChanges === 0 && (
          <p style={{ margin: '8px 0 0', color: 'var(--exo-ink-tertiary)', fontSize: 13 }}>
            No spine changes in this run — the agent's view matches what was already on file.
          </p>
        )}
      </div>
    );
  }

  // Helper for EnrichmentDiffPanel's "updated" headlines list — keep
  // the closure tiny so the JSX remains scannable.
  function _pushTo(arr, label, n) {
    if (n > 0) arr.push(`${n} ${label}${n === 1 ? '' : 's'}`);
  }

  // Coerce a Firestore Timestamp / Date / number / ISO string into ms.
  // Matches the helper in learnAboutWorkspace.js (intentionally — the
  // rate-limit guard there must read the same shape).
  function _tsToMs(ts) {
    if (ts === null || ts === undefined) return null;
    if (typeof ts === 'number') return Number.isFinite(ts) ? ts : null;
    if (ts instanceof Date) return ts.getTime();
    if (typeof ts.toMillis === 'function') {
      try { return ts.toMillis(); } catch (_e) { /* fallthrough */ }
    }
    if (typeof ts.toDate === 'function') {
      try { return ts.toDate().getTime(); } catch (_e) { /* fallthrough */ }
    }
    if (typeof ts === 'object' && typeof ts.seconds === 'number') {
      return ts.seconds * 1000 + Math.floor((ts.nanoseconds || 0) / 1e6);
    }
    if (typeof ts === 'string') {
      const ms = Date.parse(ts);
      return Number.isFinite(ms) ? ms : null;
    }
    return null;
  }

  function _relativeFromNow(ms) {
    const diffSec = Math.max(0, Math.floor((Date.now() - ms) / 1000));
    if (diffSec < 60) return 'just now';
    if (diffSec < 3600) {
      const m = Math.floor(diffSec / 60);
      return `${m} minute${m === 1 ? '' : 's'} ago`;
    }
    if (diffSec < 86400) {
      const h = Math.floor(diffSec / 3600);
      return `${h} hour${h === 1 ? '' : 's'} ago`;
    }
    const days = Math.floor(diffSec / 86400);
    if (days < 30) return `${days} day${days === 1 ? '' : 's'} ago`;
    const months = Math.floor(days / 30);
    if (months < 12) return `${months} month${months === 1 ? '' : 's'} ago`;
    const years = Math.floor(days / 365);
    return `${years} year${years === 1 ? '' : 's'} ago`;
  }

  window.Settings = Settings;
})(window);
