// =====================================================================
// OYC AI Studio — Front-end v2
//
// Lives at /v2. Shares backend (auth, plugins, chat) with v1 at /.
// v1.0 of v2 ships the redesigned dashboard only. Click-through behavior
// (Work with X) hands off to v1's chat for now (/?nav=chat-{plugin-id});
// future iterations will ship a v2 chat view.
// =====================================================================

const { useState, useEffect, useRef } = React;

const ASSETS = '/public/assets/figma-redesign';
const APP_NAME = (typeof STUDIO_CONFIG !== 'undefined' && STUDIO_CONFIG.appName) || 'OYC AI Studio';

// ---------------------------------------------------------------------
// Plugin visual mapping
// ---------------------------------------------------------------------
// API returns id, name, emoji, description, etc. — the *visual* treatment
// (which photo layers to stack, badge color, role copy) lives here.
// Keyed by plugin id from plugins/registry.json.
const PLUGIN_VISUALS = {
  'jed-pov-writer':       { firstName: 'Jed',      role: 'The AI Category POV Writer',     category: 'Strategy',  avatar: 'margaret' },
  'kendrick-mic-drop':    { firstName: 'Kendrick', role: 'The Mic Drop POV Writer',        category: 'Strategy',  avatar: 'kendrick' },
  'molly-concepts':       { firstName: 'Molly',    role: 'The Category Concepts Creator',  category: 'Strategy',  avatar: 'molly' },
  'philip-k-messaging':   { firstName: 'Philip K', role: 'The North Star Messaging Creator', category: 'Messaging', avatar: 'philip' },
  'ray-north-star-map':   { firstName: 'Ray',      role: 'The North Star Map Writer',      category: 'Messaging', avatar: 'ray' },
  'steven-p-canvas-coach':{ firstName: 'Steven P', role: 'The Category Canvas Coach',      category: 'Strategy',  avatar: 'steven' },
};

const AVATAR_LAYER_PRESETS = {
  backdropWide: layer('avatar-image-1235.png', 213, 142, '-42px', '50%', 'translateY(-50%)'),
  backdropMid: layer('avatar-image-1237.png', 153, 102, 'calc(50% + 2.5px)', '50%', 'translate(-50%, -50%)'),
  baseFace: layer('avatar-image-1246.png', 128, 128, '50%', '50%', 'translate(-50%, -50%)'),
  topHair: layer('avatar-image-1247.png', 147, 147, 'calc(50% + 3.5px)', '-38px', 'translateX(-50%)'),
};

const AVATAR_LAYERS = {
  steven: [
    AVATAR_LAYER_PRESETS.backdropWide,
    AVATAR_LAYER_PRESETS.backdropMid,
    AVATAR_LAYER_PRESETS.baseFace,
    AVATAR_LAYER_PRESETS.topHair,
  ],
  kendrick: [
    AVATAR_LAYER_PRESETS.backdropWide,
    AVATAR_LAYER_PRESETS.backdropMid,
    AVATAR_LAYER_PRESETS.baseFace,
    layer('avatar-image-1254.png', 182, 182, 'calc(50% + 4px)', 'calc(50% - 3px)', 'translate(-50%, -50%)'),
    layer('avatar-image-1258.png', 128, 128, '50%', 'calc(50% - 7px)', 'translate(-50%, -50%)'),
    layer('avatar-image-1259.png', 142, 142, '50%', 'calc(50% + 8px)', 'translate(-50%, -50%)'),
  ],
  molly: [
    layer('avatar-image-1265.png', 191, 191, 'calc(50% - 0.5px)', 'calc(50% + 9.5px)', 'translate(-50%, -50%)'),
    layer('avatar-image-1266.png', 106, 106, '50%', 'calc(50% - 5px)', 'translate(-50%, -50%)'),
  ],
  ray: [
    layer('avatar-image-1263.png', 158, 158, '50%', '50%', 'translate(-50%, -50%)'),
  ],
  philip: [
    AVATAR_LAYER_PRESETS.backdropWide,
    AVATAR_LAYER_PRESETS.backdropMid,
    layer('avatar-image-1246.png', 140, 140, 'calc(50% - 4px)', 'calc(50% + 6px)', 'translate(-50%, -50%)'),
  ],
  margaret: [
    AVATAR_LAYER_PRESETS.backdropWide,
    AVATAR_LAYER_PRESETS.backdropMid,
    AVATAR_LAYER_PRESETS.baseFace,
    AVATAR_LAYER_PRESETS.topHair,
    layer('avatar-image-1257.png', 154, 154, 'calc(50% + 2px)', 'calc(50% - 13px)', 'translate(-50%, -50%)'),
  ],
};

function layer(image, width, height, left, top, transform) {
  return { image, width, height, left, top, transform };
}

// Mirrors v1's MILESTONE_DEFS — keep in sync if v1 changes.
const MILESTONE_DEFS = [
  { id: 'first-message', emoji: '🌱', name: 'First Message', description: 'Sent your first message' },
  { id: 'chatterbox',    emoji: '💬', name: 'Chatterbox',    description: 'Sent 50 messages' },
  { id: 'power-user',    emoji: '🔥', name: 'Power User',    description: 'Sent 100 messages' },
  { id: 'super-user',    emoji: '⚡', name: 'Super User',    description: 'Sent 500 messages' },
  { id: 'legend',        emoji: '🏆', name: 'Legend',        description: 'Sent 1,000 messages' },
  { id: 'explorer',      emoji: '🧭', name: 'Explorer',      description: 'Used 3 different tools' },
  { id: 'trailblazer',   emoji: '🌍', name: 'Trailblazer',   description: 'Used every available tool' },
  { id: 'consistent',    emoji: '📅', name: 'Consistent',    description: '7-day activity streak' },
  { id: 'on-fire',       emoji: '🔥', name: 'On Fire',       description: '30-day activity streak' },
];

// ---------------------------------------------------------------------
// Cohort 2 unlock schedule (Pacific Time)
// ---------------------------------------------------------------------
// Modules and tools unlock on date boundaries in Pacific Time. Admins
// bypass all gating. Anything not in these maps is treated as unlocked.
// Date format: ISO with -07:00 (PT, daylight saving) at 00:00 = midnight
// at the start of the unlock day.
const TOOL_UNLOCKS = {
  // plugin id → ISO unlock instant. Mirrors module dates.
  'steven-p-canvas-coach': null,                       // Week 1 — always unlocked
  'kendrick-mic-drop':     '2026-05-18T00:00:00-07:00', // Week 2
  'molly-concepts':        '2026-05-18T00:00:00-07:00', // Week 2
  'ray-north-star-map':    '2026-05-25T00:00:00-07:00', // Week 3
  'philip-k-messaging':    '2026-06-01T00:00:00-07:00', // Week 4
  'jed-pov-writer':        '2026-06-01T00:00:00-07:00', // Week 4 (Margaret runs as KV override on this id)
};

const LESSON_UNLOCKS = {
  'Welcome': null,
  'Module 1: Build Your Category Canvas': null,
  'Module 2: Write Your Mic Drop POV': '2026-05-18T00:00:00-07:00',
  'Module 3: Create Your North Star Map': '2026-05-25T00:00:00-07:00',
  'Module 4: Finalize Your North Star Messaging': '2026-06-01T00:00:00-07:00',
  'Wrap Up & Next Steps': '2026-06-03T00:00:00-07:00',
};

function isUnlockedFor(unlockIso, user, now) {
  if (user?.role === 'admin') return true;
  if (!unlockIso) return true;
  const at = Date.parse(unlockIso);
  if (Number.isNaN(at)) return true;
  return (now ?? Date.now()) >= at;
}

function isLessonUnlockedFor(unlockIso, user, now) {
  return isUnlockedFor(unlockIso, user, now);
}

function formatUnlockDate(unlockIso) {
  // "Mon, May 18" — short and friendly for the locked-row hint.
  const d = new Date(unlockIso);
  return d.toLocaleDateString('en-US', {
    weekday: 'short', month: 'short', day: 'numeric', timeZone: 'America/Los_Angeles',
  });
}

// Inline lock glyph. 14×14 stroke icon.
function LockIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
         strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <rect x="4" y="11" width="16" height="10" rx="2"/>
      <path d="M8 11V7a4 4 0 0 1 8 0v4"/>
    </svg>
  );
}

// ---------------------------------------------------------------------
// Components
// ---------------------------------------------------------------------

function Avatar({ visuals, size }) {
  // size: 'md' | 'sm' (defaults to full 90px dashboard size)
  const sizeClass = size ? ` avatar--${size}` : '';
  return (
    <div className={`avatar${sizeClass}`}>
      <PluginAvatarLayers visuals={visuals} />
    </div>
  );
}

function PluginCard({ plugin, visuals, isFavorite, onOpen, onToggleFavorite, locked, unlockIso }) {
  // Display fields come from the dynamic plugin manifest (admin-editable),
  // with PLUGIN_VISUALS as fallback for back-compat. Avatar layers are
  // visual-only and stay on PLUGIN_VISUALS.
  const displayName = plugin.name || visuals.firstName;
  const displayRole = plugin.role || visuals.role;
  const tagLower = (plugin.tag || visuals.category || '').toLowerCase();
  const tagLabel = plugin.tag
    ? plugin.tag.charAt(0).toUpperCase() + plugin.tag.slice(1)
    : visuals.category;
  const badgeClass = tagLower === 'messaging' ? 'badge--messaging' : 'badge--strategy';
  const starIcon = isFavorite ? 'icon-star-filled.svg' : 'icon-star-outline.svg';
  const favBtnClass = isFavorite ? 'btn-icon' : 'btn-icon btn-icon--outline';
  const unlockHint = locked && unlockIso
    ? `Available ${formatUnlockDate(unlockIso)}`
    : null;

  return (
    <article className={`card${locked ? ' card--locked' : ''}`} aria-disabled={locked || undefined}>
      <div className="card-header">
        <Avatar visuals={visuals} />
        <span className={`badge ${badgeClass}`}>{tagLabel}</span>
      </div>
      <div className="card-body">
        <h3 className="plugin-name">
          {displayName}, <span className="plugin-role">{displayRole}</span>
        </h3>
        <p className="plugin-desc">{plugin.description}</p>
        {unlockHint && (
          <p className="plugin-locked-hint"><LockIcon /> {unlockHint}</p>
        )}
      </div>
      <div className="card-footer">
        <button
          className="btn-primary"
          onClick={() => { if (!locked) onOpen(plugin.id); }}
          disabled={locked}
          title={unlockHint || undefined}
        >
          <span className="btn-primary-textwrap">
            <span className="btn-primary-label">
              {locked ? `Unlocks ${formatUnlockDate(unlockIso)}` : `Work with ${displayName}`}
            </span>
            <span className="btn-primary-arrow" aria-hidden="true">{locked ? '🔒' : '→'}</span>
          </span>
        </button>
        <button
          className={favBtnClass}
          aria-label="Favorite"
          onClick={() => { if (!locked) onToggleFavorite(plugin.id); }}
          disabled={locked}
        >
          <img src={`${ASSETS}/${starIcon}`} alt="" />
        </button>
      </div>
    </article>
  );
}

function Sidebar({ user, activeNav, onNav, onLogout, pinnedChats, onOpenPinnedChat, onRenamePinnedChat, plugins, onOpenPlugin, theme, onToggleTheme }) {
  const initials = user
    ? (user.display_name || user.email || '?').split(/[\s@.]+/).filter(Boolean).slice(0, 2).map(s => s[0]).join('').toUpperCase()
    : '?';
  const displayName = user?.display_name || user?.email || 'You';
  const [renamingId, setRenamingId] = useState(null);
  const [renameDraft, setRenameDraft] = useState('');

  const startRename = (e, convo) => {
    e.stopPropagation();
    setRenamingId(convo.id);
    setRenameDraft(convo.title || '');
  };
  const commitRename = async (id) => {
    const title = renameDraft.trim();
    if (title) await onRenamePinnedChat?.(id, title);
    setRenamingId(null);
  };

  return (
    <aside className="sidebar">
      <div className="sidebar-scroll">
      <button className="brand brand--clickable" onClick={() => onNav('dashboard')} title="Back to dashboard">
        OYC<span className="reg">®</span>
      </button>

      <nav className="nav">
        <button className={`nav-item ${activeNav === 'dashboard' ? 'nav-item--active' : 'nav-item--inactive'}`} onClick={() => onNav('dashboard')}>
          <img className="nav-icon" src={`${ASSETS}/icon-widget.svg`} alt="" />
          <span>Dashboard</span>
        </button>
        <button className={`nav-item ${activeNav === 'tools' ? 'nav-item--active' : 'nav-item--inactive'}`} onClick={() => onNav('tools')}>
          <img className="nav-icon" src={`${ASSETS}/icon-ai-edit.svg`} alt="" />
          <span>My Tools</span>
        </button>
        <button className={`nav-item ${activeNav === 'training' ? 'nav-item--active' : 'nav-item--inactive'}`} onClick={() => onNav('training')}>
          <img className="nav-icon" src={`${ASSETS}/icon-quality-education.svg`} alt="" />
          <span>Training</span>
        </button>
        <button className={`nav-item ${activeNav === 'settings' ? 'nav-item--active' : 'nav-item--inactive'}`} onClick={() => onNav('settings')}>
          <img className="nav-icon" src={`${ASSETS}/icon-ai-settings.svg`} alt="" />
          <span>Settings</span>
        </button>
        {user?.role === 'admin' && (
          <button className={`nav-item ${activeNav === 'admin' ? 'nav-item--active' : 'nav-item--inactive'}`} onClick={() => onNav('admin')}>
            <span className="nav-icon" aria-hidden="true">
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                <path d="M12 1L3 5v6c0 5.5 3.8 10.7 9 12 5.2-1.3 9-6.5 9-12V5l-9-4z"/>
              </svg>
            </span>
            <span>Admin</span>
          </button>
        )}
      </nav>

      {(() => {
        const favoriteIds = user?.favorite_plugins || [];
        const pinnedTools = (plugins || []).filter(p => favoriteIds.includes(p.id));
        return (
          <>
            <div className="sidebar-pinned">
              <div className="sidebar-pinned-label">Tools</div>
              {pinnedTools.length === 0 ? (
                <div className="sidebar-pinned-empty">Pin tools to find them here.</div>
              ) : (
                <div className="sidebar-pinned-list">
                  {pinnedTools.map(p => {
                    const v = PLUGIN_VISUALS[p.id];
                    const firstName = p.name || v?.firstName || p.id;
                    const role = p.role || v?.role || '';
                    return (
                      <button
                        key={p.id}
                        className="pinned-tool"
                        onClick={() => onOpenPlugin?.(p.id)}
                        title={role ? `${firstName}, ${role}` : firstName}
                      >
                        {v?.avatar && (
                          <div className="avatar avatar--xs">
                            <PluginAvatarLayers visuals={v} />
                          </div>
                        )}
                        <span className="pinned-tool-label">
                          <span className="pinned-tool-name">{firstName}</span>
                          {role && <span className="pinned-tool-role">{role}</span>}
                        </span>
                      </button>
                    );
                  })}
                </div>
              )}
            </div>

            <div className="sidebar-pinned">
              <div className="sidebar-pinned-label">Conversations</div>
              {!pinnedChats || pinnedChats.length === 0 ? (
                <div className="sidebar-pinned-empty">Pin chats to find them here.</div>
              ) : (
                <div className="sidebar-pinned-list">
                  {pinnedChats.map(c => {
                    if (renamingId === c.id) {
                      return (
                        <div key={c.id} className="pinned-chat pinned-chat--renaming" onClick={e => e.stopPropagation()}>
                          <input
                            className="pinned-chat-rename-input"
                            value={renameDraft}
                            onChange={e => setRenameDraft(e.target.value)}
                            onKeyDown={e => {
                              if (e.key === 'Enter') commitRename(c.id);
                              if (e.key === 'Escape') setRenamingId(null);
                            }}
                            onBlur={() => commitRename(c.id)}
                            autoFocus
                          />
                        </div>
                      );
                    }
                    return (
                      <div key={c.id} className="pinned-chat-row">
                        <button
                          className="pinned-chat"
                          onClick={() => onOpenPinnedChat?.(c)}
                          title={c.title || 'Untitled'}
                        >
                          <span className="pinned-chat-title">{c.title || 'Untitled'}</span>
                        </button>
                        <button
                          className="pinned-chat-rename"
                          onClick={(e) => startRename(e, c)}
                          aria-label="Rename"
                          title="Rename"
                        >✎</button>
                      </div>
                    );
                  })}
                </div>
              )}
            </div>
          </>
        );
      })()}
      </div>

      <div className="sidebar-footer">
        {(() => {
          const cfg = (typeof STUDIO_CONFIG !== 'undefined') ? STUDIO_CONFIG : {};
          const plans = cfg.plans || [];
          const currentPlan = plans.find(p => p.id === user?.plan);
          const nextPlan = plans.find(p => p.price > (currentPlan?.price || 0));
          const planName = currentPlan?.name || (user?.plan ? user.plan.charAt(0).toUpperCase() + user.plan.slice(1) : 'Free');
          return (
            <div className="plan-card">
              <p className="plan-label">Your plan</p>
              <p className="plan-tier">{planName}</p>
              {nextPlan ? (
                <button className="plan-upgrade" onClick={() => onNav('pricing')}>
                  Upgrade to {nextPlan.name}
                  <img className="arrow" src={`${ASSETS}/icon-arrow-right.svg`} alt="" />
                </button>
              ) : (
                <button className="plan-upgrade" onClick={openBillingPortal}>
                  Manage billing
                  <img className="arrow" src={`${ASSETS}/icon-arrow-right.svg`} alt="" />
                </button>
              )}
            </div>
          );
        })()}
        <div className="user-bar">
          <div className="avatar-mono avatar-mono--initial">{initials}</div>
          <div className="user-text">
            <span className="user-name">{displayName}</span>
            <button className="user-action" onClick={onLogout}>Sign Out</button>
          </div>
          {onToggleTheme && (
            <button className="theme-toggle" onClick={onToggleTheme} title={theme === 'dark' ? 'Switch to light' : 'Switch to dark'} aria-label="Toggle theme">
              {theme === 'dark' ? (
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                  <circle cx="12" cy="12" r="4"/>
                  <line x1="12" y1="2" x2="12" y2="5"/>
                  <line x1="12" y1="19" x2="12" y2="22"/>
                  <line x1="2" y1="12" x2="5" y2="12"/>
                  <line x1="19" y1="12" x2="22" y2="12"/>
                  <line x1="4.93" y1="4.93" x2="7.05" y2="7.05"/>
                  <line x1="16.95" y1="16.95" x2="19.07" y2="19.07"/>
                  <line x1="4.93" y1="19.07" x2="7.05" y2="16.95"/>
                  <line x1="16.95" y1="7.05" x2="19.07" y2="4.93"/>
                </svg>
              ) : (
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
                </svg>
              )}
            </button>
          )}
        </div>
      </div>
    </aside>
  );
}

// =====================================================================
// EmailVerificationBanner — surfaced above main when email_verified=false.
// =====================================================================
function EmailVerificationBanner() {
  const [resending, setResending] = useState(false);
  const [done, setDone] = useState(false);
  const handleResend = async () => {
    setResending(true);
    try {
      const res = await fetch('/api/auth/resend-verify', { method: 'POST', credentials: 'include' });
      if (res.ok) setDone(true);
    } catch {}
    setResending(false);
  };
  return (
    <div className="verification-banner">
      <span>Please verify your email to keep using all features.</span>
      {done ? (
        <span className="verification-banner-status">✓ Verification email sent</span>
      ) : (
        <button className="verification-banner-cta" onClick={handleResend} disabled={resending}>
          {resending ? 'Sending…' : 'Resend verification'}
        </button>
      )}
    </div>
  );
}

// =====================================================================
// Toast — short-lived top-right notification (success/error/info).
// =====================================================================
function ToastStack({ toasts, onDismiss }) {
  return (
    <div className="toast-stack" aria-live="polite">
      {toasts.map(t => (
        <Toast key={t.id} toast={t} onDismiss={onDismiss} />
      ))}
    </div>
  );
}
function Toast({ toast, onDismiss }) {
  useEffect(() => {
    const t = setTimeout(() => onDismiss(toast.id), 4000);
    return () => clearTimeout(t);
  }, [toast.id]);
  return (
    <div className={`toast toast--${toast.kind || 'info'}`} onClick={() => onDismiss(toast.id)}>
      {toast.message}
    </div>
  );
}

// =====================================================================
// AppShell — the global v2 chrome. Every authenticated page wraps its
// content with this so the sidebar, landscape backdrop, and version
// toggle stay consistent across the app.
// =====================================================================
function AppShell({
  user, activeNav, onNav, onLogout, pinnedChats, onOpenPinnedChat, onRenamePinnedChat,
  plugins, onOpenPlugin,
  theme, onToggleTheme, children,
  historyOpen, historyConvos, onCloseHistory, onOpenHistoryConvo, onDeleteHistoryConvo, onRenameHistoryConvo,
}) {
  const showVerifyBanner = user && !user.email_verified;
  return (
    <div className="page">
      <Sidebar
        user={user}
        activeNav={activeNav}
        onNav={onNav}
        onLogout={onLogout}
        pinnedChats={pinnedChats}
        onOpenPinnedChat={onOpenPinnedChat}
        onRenamePinnedChat={onRenamePinnedChat}
        plugins={plugins}
        onOpenPlugin={onOpenPlugin}
        theme={theme}
        onToggleTheme={onToggleTheme}
      />

      <HistoryDrawer
        open={historyOpen}
        convos={historyConvos}
        onClose={onCloseHistory}
        onOpenConvo={onOpenHistoryConvo}
        onDelete={onDeleteHistoryConvo}
        onRename={onRenameHistoryConvo}
      />

      <div className="main-area">
        {showVerifyBanner && <EmailVerificationBanner />}
        <main className="main">
          {children}
        </main>
      </div>

      <img className="landscape" src={`${ASSETS}/landscape-hero.png`} alt="" />

    </div>
  );
}

function formatRelativeDate(iso) {
  if (!iso) return '';
  const date = new Date(iso);
  const diff = (Date.now() - date.getTime()) / 1000;
  if (diff < 60) return 'Just now';
  if (diff < 3600) return `${Math.floor(diff / 60)}m`;
  if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
  if (diff < 604800) return `${Math.floor(diff / 86400)}d`;
  return date.toLocaleDateString();
}

function HistoryDrawer({ open, convos, onClose, onOpenConvo, onDelete, onRename }) {
  const [renamingId, setRenamingId] = useState(null);
  const [renameDraft, setRenameDraft] = useState('');

  const startRename = (e, c) => {
    e.stopPropagation();
    setRenamingId(c.id);
    setRenameDraft(c.title || '');
  };
  const commitRename = async (id) => {
    const title = renameDraft.trim();
    if (title) await onRename(id, title);
    setRenamingId(null);
  };

  return (
    <aside className={`history-drawer${open ? ' history-drawer--open' : ''}`} aria-hidden={!open}>
      <div className="history-drawer-header">
        <span className="history-drawer-title">All conversations</span>
        <button className="history-drawer-close" onClick={onClose} aria-label="Close history">×</button>
      </div>
      <div className="history-drawer-list">
        {convos.length === 0 ? (
          <div className="history-empty">No past conversations.</div>
        ) : (
          convos.map(c => {
            if (renamingId === c.id) {
              return (
                <div key={c.id} className="history-item" onClick={e => e.stopPropagation()}>
                  <input
                    className="history-rename-input"
                    value={renameDraft}
                    onChange={e => setRenameDraft(e.target.value)}
                    onKeyDown={e => {
                      if (e.key === 'Enter') commitRename(c.id);
                      if (e.key === 'Escape') setRenamingId(null);
                    }}
                    autoFocus
                  />
                  <button className="history-item-delete" onClick={() => commitRename(c.id)} aria-label="Save">✓</button>
                  <button className="history-item-delete" onClick={() => setRenamingId(null)} aria-label="Cancel">×</button>
                </div>
              );
            }
            return (
              <div
                key={c.id}
                className="history-item"
                role="button"
                tabIndex={0}
                onClick={() => onOpenConvo(c)}
                onKeyDown={(e) => {
                  if (e.target !== e.currentTarget) return;
                  if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    onOpenConvo(c);
                  }
                }}
              >
                <span className="history-item-text">
                  <span className="history-item-title">{c.title || 'Untitled'}</span>
                  <span className="history-item-date">{formatRelativeDate(c.updated_at || c.created_at)}</span>
                </span>
                <button className="history-item-delete" onClick={(e) => startRename(e, c)} aria-label="Rename">✎</button>
                <button className="history-item-delete" onClick={(e) => { e.stopPropagation(); onDelete(c.id); }} aria-label="Delete">×</button>
              </div>
            );
          })
        )}
      </div>
    </aside>
  );
}

// ---------------------------------------------------------------------
// Page contents — each fits inside <main> within the AppShell.
// ---------------------------------------------------------------------

// =====================================================================
// ToolsContent — "My Tools" page. The 6 plugin cards on the landscape.
// (Was previously misnamed DashboardContent.)
// =====================================================================
function ToolsContent({ plugins, user, onOpenPlugin, onToggleFavorite }) {
  const favorites = user?.favorite_plugins || [];
  const now = Date.now();
  return (
    <>
      <header className="hero">
        <h1 className="hero-title">Different happens here.</h1>
        <p className="hero-sub">Stop competing. Start defining your category.</p>
      </header>

      <div className="card-grid">
        {plugins.map(plugin => {
          const visuals = PLUGIN_VISUALS[plugin.id];
          if (!visuals) return null;
          const unlockIso = TOOL_UNLOCKS[plugin.id];
          const locked = !isUnlockedFor(unlockIso, user, now);
          return (
            <PluginCard
              key={plugin.id}
              plugin={plugin}
              visuals={visuals}
              isFavorite={favorites.includes(plugin.id)}
              onOpen={onOpenPlugin}
              onToggleFavorite={onToggleFavorite}
              locked={locked}
              unlockIso={unlockIso}
            />
          );
        })}
      </div>
    </>
  );
}

// =====================================================================
// DashboardContent — personal home. Mirrors v1's DashboardHome IA:
// greeting, stat cards (admin or user variant), personal activity,
// weekly + all-time leaderboards, milestones, upgrade upsell.
// Hits same backend: /api/stats/me, /api/leaderboard, /api/admin/usage.
// =====================================================================

// Inline line-style icons used by stat cards + leaderboard headers.
// Hand-rolled to match Streamline Core aesthetic at 24×24 with stroke 1.5.
function Icon({ name, size = 24 }) {
  const props = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' };
  switch (name) {
    case 'chat-bubble':
      return (
        <svg {...props}>
          <path d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-7l-4 4v-4H5a2 2 0 0 1-2-2V5z"/>
          <line x1="7" y1="8" x2="17" y2="8"/>
          <line x1="7" y1="12" x2="13" y2="12"/>
        </svg>
      );
    case 'signal':
      return (
        <svg {...props}>
          <line x1="6" y1="20" x2="6" y2="14"/>
          <line x1="12" y1="20" x2="12" y2="10"/>
          <line x1="18" y1="20" x2="18" y2="4"/>
        </svg>
      );
    case 'wrench':
      return (
        <svg {...props}>
          <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
        </svg>
      );
    case 'flash':
      return (
        <svg {...props}>
          <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
        </svg>
      );
    case 'calendar':
      return (
        <svg {...props}>
          <rect x="3" y="4" width="18" height="18" rx="2"/>
          <line x1="16" y1="2" x2="16" y2="6"/>
          <line x1="8" y1="2" x2="8" y2="6"/>
          <line x1="3" y1="10" x2="21" y2="10"/>
        </svg>
      );
    case 'compass':
      return (
        <svg {...props}>
          <circle cx="12" cy="12" r="10"/>
          <polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>
        </svg>
      );
    case 'trophy':
      return (
        <svg {...props}>
          <path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/>
          <path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/>
          <path d="M4 22h16"/>
          <path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/>
          <path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/>
          <path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/>
        </svg>
      );
    case 'users':
      return (
        <svg {...props}>
          <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
          <circle cx="9" cy="7" r="4"/>
          <path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
          <path d="M16 3.13a4 4 0 0 1 0 7.75"/>
        </svg>
      );
    case 'dollar':
      return (
        <svg {...props}>
          <line x1="12" y1="1" x2="12" y2="23"/>
          <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
        </svg>
      );
    default:
      return null;
  }
}

function StatCard({ icon, value, label, progress, valueSize }) {
  const valueClass = valueSize === 'half'
    ? 'stat-card-value stat-card-value--half'
    : 'stat-card-value';

  return (
    <div className="stat-card">
      <div className="stat-card-icon"><Icon name={icon} /></div>
      <div className="stat-card-text">
        <div className={valueClass}>{value}</div>
        <div className="stat-card-label">{label}</div>
        {progress !== undefined && (
          <div className="stat-card-progress">
            <div style={{ width: `${progress}%` }} />
          </div>
        )}
      </div>
    </div>
  );
}

function LeaderboardPanel({ icon, label, rows, labelFn }) {
  return (
    <div className="lb-panel">
      <div className="lb-panel-header">
        <span className="lb-panel-header-icon"><Icon name={icon} /></span>
        <span className="lb-panel-header-label">{label}</span>
      </div>
      <div className="lb-panel-rows">
        {rows.slice(0, 3).map((row, i) => {
          const rankClass = i === 1 ? ' lb-rank-pill--silver' : i === 2 ? ' lb-rank-pill--bronze' : '';
          return (
            <div key={i} className="lb-panel-row">
              <span className={`lb-rank-pill${rankClass}`}>#{i + 1}</span>
              <span className="lb-name">{labelFn(row)}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------
// Stripe helpers — used by every "upgrade" / "manage billing" CTA in v2.
// ---------------------------------------------------------------------
async function startCheckout(planId, interval) {
  if (!planId) return;
  try {
    const res = await fetch('/api/stripe/checkout', {
      method: 'POST',
      credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ planId, interval: interval || 'monthly' }),
    });
    const data = await res.json().catch(() => ({}));
    if (data.url) {
      window.location.href = data.url;
    } else {
      window.alert(data.error || 'Could not start checkout. Try again.');
    }
  } catch {
    window.alert('Could not start checkout. Try again.');
  }
}

async function openBillingPortal() {
  try {
    const res = await fetch('/api/stripe/portal', {
      method: 'POST',
      credentials: 'include',
    });
    const data = await res.json().catch(() => ({}));
    if (data.url) {
      window.location.href = data.url;
    } else {
      window.alert(data.error || 'Could not open billing portal.');
    }
  } catch {
    window.alert('Could not connect to billing portal.');
  }
}

function pluginDisplayName(pluginId, plugins) {
  const plugin = plugins.find(p => p.id === pluginId);
  if (plugin?.name) return plugin.name;
  const visuals = PLUGIN_VISUALS[pluginId];
  return visuals?.firstName || pluginId;
}

function computeApiCost(adminStats) {
  const pricing = (typeof STUDIO_CONFIG !== 'undefined' && STUDIO_CONFIG.apiPricing) || {};
  return (adminStats.byPlugin || []).reduce((sum, r) => {
    const p = pricing[r.model] || { inputPer1k: 0.003, outputPer1k: 0.015, cacheWritePer1k: 0.00375, cacheReadPer1k: 0.0003 };
    return sum
      + (r.input_tokens / 1000) * p.inputPer1k
      + (r.output_tokens / 1000) * p.outputPer1k
      + ((r.cache_creation_input_tokens || 0) / 1000) * (p.cacheWritePer1k || 0.00375)
      + ((r.cache_read_input_tokens || 0) / 1000) * (p.cacheReadPer1k || 0.0003);
  }, 0).toFixed(2);
}

function DashboardContent({ user, plugins, onNav }) {
  const isAdmin = user?.role === 'admin';
  const [adminStats, setAdminStats] = useState(null);
  const [personalStats, setPersonalStats] = useState(null);
  const [leaderboard, setLeaderboard] = useState(null);

  // Greeting copy comes from the Figma design — single phrase, no time-of-day variation.
  const firstName = (user?.full_name || user?.display_name || user?.email || '').split(/[\s@]/)[0] || '';

  useEffect(() => {
    if (!isAdmin) return;
    (async () => {
      try {
        const res = await fetch('/api/admin/usage?days=30', { credentials: 'include' });
        if (res.ok) setAdminStats(await res.json());
      } catch {}
    })();
  }, [isAdmin]);

  useEffect(() => {
    (async () => {
      try {
        const [statsRes, lbRes] = await Promise.all([
          fetch('/api/stats/me', { credentials: 'include' }),
          fetch('/api/leaderboard', { credentials: 'include' }),
        ]);
        if (statsRes.ok) setPersonalStats(await statsRes.json());
        if (lbRes.ok) setLeaderboard(await lbRes.json());
      } catch {}
    })();
  }, []);

  const cfg = (typeof STUDIO_CONFIG !== 'undefined') ? STUDIO_CONFIG : {};
  const planLimits = { free: cfg.rateLimits?.freeTrialMessages || 10 };
  (cfg.plans || []).forEach(p => { planLimits[p.id] = p.monthlyMessageLimit; });
  const limit = planLimits[user?.plan] || 10;
  const used = user?.messages_used || 0;
  const remaining = Math.max(limit - used, 0);

  const plans = cfg.plans || [];
  const currentPlan = plans.find(p => p.id === user?.plan);
  const upgradePlan = plans.find(p => p.price > (currentPlan?.price || 0));

  const stats = isAdmin ? [
    { icon: 'users',       label: 'Total Users',    value: adminStats ? String(adminStats.totalUsers || 0) : '—' },
    { icon: 'signal',      label: 'Active (30d)',   value: adminStats ? String(adminStats.activeUsers || 0) : '—' },
    { icon: 'chat-bubble', label: 'Messages (30d)', value: adminStats ? (adminStats.totals?.total_messages || 0).toLocaleString() : '—' },
    { icon: 'dollar',      label: 'Est. API Cost',  value: adminStats ? '$' + computeApiCost(adminStats) : '—' },
  ] : [
    { icon: 'chat-bubble', label: 'Messages Left', value: String(remaining), valueSize: 'half' },
    { icon: 'signal',      label: 'Messages Used',      value: String(used) },
    { icon: 'wrench',      label: 'Tools Available',    value: String(plugins.length) },
    { icon: 'flash',       label: 'Current Plan',       value: ((user?.plan || 'free').charAt(0).toUpperCase() + (user?.plan || 'free').slice(1)), valueSize: 'half' },
  ];

  const handleUpgrade = () => onNav?.('pricing');

  return (
    <div className="dashboard-content">
      <div className="dashboard-greeting">
        <h1 className="hero-title">Let's do this{firstName ? `, ${firstName}` : ''}.</h1>
        <p className="hero-sub">What are we building today?</p>
      </div>

      {/* Top stat row — no chip header, sits directly under greeting */}
      <div className="stat-grid">
        {stats.map(s => (
          <StatCard key={s.label} icon={s.icon} value={s.value} label={s.label} valueSize={s.valueSize} />
        ))}
      </div>

      {personalStats && (
        <section className="dash-section">
          <div className="section-chip">Your Activity</div>
          <div className="stat-grid">
            <StatCard
              icon={personalStats.streak > 0 ? 'flash' : 'calendar'}
              value={personalStats.streak || 0}
              label="Day Streak"
              progress={0}
            />
            <StatCard
              icon="chat-bubble"
              value={personalStats.messagesThisWeek || 0}
              label="Messages This Week"
            />
            <StatCard
              icon="signal"
              value={personalStats.messagesThisMonth || 0}
              label="Messages Used This Month"
            />
            <StatCard
              icon="compass"
              value={`${personalStats.toolsExplored || 0}/${personalStats.totalTools || 0}`}
              label="Tools Explored"
              progress={personalStats.totalTools > 0 ? Math.min((personalStats.toolsExplored / personalStats.totalTools) * 100, 100) : undefined}
            />
          </div>
        </section>
      )}

      {leaderboard && (
        <section className="dash-section">
          <div className="section-chip">This Week's Leaderboard</div>
          {(leaderboard.topUsers?.length > 0 || leaderboard.topTools?.length > 0) ? (
            <div className="lb-grid">
              <LeaderboardPanel
                icon="trophy"
                label="TOP USERS"
                rows={leaderboard.topUsers || []}
                labelFn={u => u.displayName}
              />
              <LeaderboardPanel
                icon="wrench"
                label="POPULAR TOOLS (This Week)"
                rows={leaderboard.topTools || []}
                labelFn={t => pluginDisplayName(t.pluginId, plugins)}
              />
            </div>
          ) : (
            <div className="lb-empty">No activity yet this week — check back soon.</div>
          )}
        </section>
      )}

      {leaderboard && (leaderboard.topUsersAllTime?.length > 0 || leaderboard.topToolsAllTime?.length > 0) && (
        <section className="dash-section">
          <div className="section-chip">All Time Leaderboard</div>
          <div className="lb-grid">
            {leaderboard.topUsersAllTime?.length > 0 && (
              <LeaderboardPanel
                icon="trophy"
                label="TOP USERS"
                rows={leaderboard.topUsersAllTime}
                labelFn={u => u.displayName}
              />
            )}
            {leaderboard.topToolsAllTime?.length > 0 && (
              <LeaderboardPanel
                icon="wrench"
                label="POPULAR TOOLS"
                rows={leaderboard.topToolsAllTime}
                labelFn={t => pluginDisplayName(t.pluginId, plugins)}
              />
            )}
          </div>
        </section>
      )}

      {personalStats?.milestones !== undefined && (
        <section className="dash-section">
          <div className="section-chip">Milestones</div>
          <div className="milestone-grid">
            {MILESTONE_DEFS.map(m => {
              const earned = (personalStats.milestones || []).find(e => e.id === m.id);
              return (
                <div key={m.id} className={`milestone-tile ${earned ? 'milestone-tile--earned' : ''}`}>
                  <div className="milestone-emoji">{m.emoji}</div>
                  <div className="milestone-name">{m.name}</div>
                  <div className="milestone-desc">{m.description}</div>
                  {earned && <div className="milestone-date">{new Date(earned.earnedAt).toLocaleDateString()}</div>}
                </div>
              );
            })}
          </div>
        </section>
      )}

      {upgradePlan && !isAdmin && (
        <div className="upgrade-card">
          <div>
            <div className="upgrade-eyebrow">⚡ {upgradePlan.name}</div>
            <div className="upgrade-title">Want to level up?</div>
            <div className="upgrade-detail">{(upgradePlan.features || []).slice(-2).join(' + ')}. ${upgradePlan.price}/{upgradePlan.period}.</div>
          </div>
          <button className="upgrade-cta" onClick={handleUpgrade}>Join {upgradePlan.name}</button>
        </div>
      )}
    </div>
  );
}

// =====================================================================
// Chat — conversation screen for a single plugin.
// Hits the same /api/chat (SSE-streaming) backend as v1.
// =====================================================================

function PluginAvatarLayers({ visuals }) {
  const layers = AVATAR_LAYERS[visuals?.avatar] || [];
  return (
    <div className="avatar-stack">
      {layers.map((l, i) => (
        <img
          key={`${l.image}-${i}`}
          className="avatar-layer"
          src={`${ASSETS}/${l.image}`}
          alt=""
          style={{
            width: `${l.width}px`,
            height: `${l.height}px`,
            left: l.left,
            top: l.top,
            transform: l.transform,
          }}
        />
      ))}
    </div>
  );
}

function renderMarkdown(text) {
  if (!text) return '';
  if (typeof marked === 'undefined') return escapeHtml(text);
  try {
    // breaks: false on purpose. With breaks: true every single newline turns
    // into a <br>, including the newline at the end of a paragraph that
    // precedes a block element (list, heading, blockquote). That phantom <br>
    // adds an extra line of whitespace between a paragraph and the list/header
    // that follows it. Standard markdown semantics: blank lines separate
    // paragraphs; single newlines mid-paragraph collapse to spaces.
    const html = marked.parse(text, { breaks: false, gfm: true });
    if (typeof DOMPurify !== 'undefined') {
      return DOMPurify.sanitize(html, {
        USE_PROFILES: { html: true },
        ADD_ATTR: ['target', 'rel'],
      });
    }
    return escapeHtml(text);
  } catch { return escapeHtml(text); }
}

function escapeHtml(text) {
  return String(text).replace(/[&<>"']/g, ch => ({
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
  }[ch]));
}

function ChatMessage({ role, content, visuals, images, docs, onEdit, onRegenerate }) {
  const isAssistant = role === 'assistant';
  if (role === 'compaction') {
    return (
      <div style={{ alignSelf: 'center', fontSize: 12, color: 'var(--c-ink-50)', padding: '4px 12px' }}>
        — {content} —
      </div>
    );
  }
  return (
    <div className={`msg msg--${role}`}>
      {isAssistant && (
        <div className="avatar avatar--sm">
          <PluginAvatarLayers visuals={visuals} />
        </div>
      )}
      <div className="msg-bubble">
        {images && images.length > 0 && (
          <div className="msg-images">
            {images.map((src, i) => <img key={i} src={src} alt="" />)}
          </div>
        )}
        {docs && docs.length > 0 && (
          <div className="msg-docs">
            {docs.map((d, i) => <span key={i}>📄 {d.name}</span>)}
          </div>
        )}
        {isAssistant
          ? <div className="msg-md" dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }} />
          : (content || (images?.length ? '' : ''))
        }
        {(onEdit || onRegenerate) && (
          <div className="msg-actions">
            {onEdit && !isAssistant && (
              <button className="msg-action" onClick={onEdit} title="Edit & resend">✎</button>
            )}
            {onRegenerate && isAssistant && content && (
              <button className="msg-action" onClick={onRegenerate} title="Regenerate response">↻</button>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

function ThinkingMessage({ visuals }) {
  return (
    <div className="msg msg--assistant">
      <div className="avatar avatar--sm">
        <PluginAvatarLayers visuals={visuals} />
      </div>
      <div className="msg-bubble msg-bubble--thinking">
        <span className="thinking-dot"></span>
        <span className="thinking-dot"></span>
        <span className="thinking-dot"></span>
      </div>
    </div>
  );
}

function ChatScreen({ plugin, user, initialConversationId, onConversationChange, onPinnedChange, onOpenHistory }) {
  const visuals = PLUGIN_VISUALS[plugin.id];
  const firstName = plugin.name || visuals?.firstName;
  const role = plugin.role || visuals?.role || plugin.description || '';
  const tagLower = (plugin.tag || visuals?.category || '').toLowerCase();
  const tagLabel = plugin.tag
    ? plugin.tag.charAt(0).toUpperCase() + plugin.tag.slice(1)
    : visuals?.category;

  const greeting = plugin.greeting || `Hi, I'm ${firstName}. What are you working on?`;

  const [messages, setMessages] = useState([{ role: 'assistant', content: greeting }]);
  const [conversationId, setConversationId] = useState(initialConversationId || null);
  const [isPinned, setIsPinned] = useState(false);
  const [input, setInput] = useState('');
  const [isThinking, setIsThinking] = useState(false);
  const [pendingImages, setPendingImages] = useState([]);
  const [pendingDocs, setPendingDocs] = useState([]);
  const [attachError, setAttachError] = useState('');

  const messagesEndRef = useRef(null);
  const inputRef = useRef(null);
  const fileInputRef = useRef(null);

  // Reset when plugin changes (only if no specific convo to load)
  useEffect(() => {
    if (!initialConversationId) {
      setMessages([{ role: 'assistant', content: greeting }]);
      setConversationId(null);
      setIsPinned(false);
    }
  }, [plugin.id]);

  // Track conversation IDs we minted locally (after sending the first message in
  // a new chat). When the parent echoes that ID back as initialConversationId,
  // we must NOT refetch — the worker writes the user+assistant messages to D1
  // *after* the stream completes, so a fetch here races the batch INSERT and
  // returns an empty conversation, wiping the messages we just streamed.
  const locallyOwnedConvoIds = useRef(new Set());

  // Load specific conversation (e.g. clicked from sidebar pinned chat or history)
  useEffect(() => {
    if (!initialConversationId) return;
    if (locallyOwnedConvoIds.current.has(initialConversationId)) return;
    (async () => {
      try {
        const res = await fetch(`/api/conversations/${initialConversationId}`, { credentials: 'include' });
        if (res.ok) {
          const data = await res.json();
          setMessages(data.messages || []);
          setConversationId(initialConversationId);
          setIsPinned(!!data.conversation?.pinned);
        }
      } catch {}
    })();
  }, [initialConversationId]);

  // Auto-scroll on new messages / thinking state
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });
  }, [messages, isThinking]);

  // Auto-grow textarea
  const handleInputChange = (e) => {
    setInput(e.target.value);
    if (inputRef.current) {
      inputRef.current.style.height = 'auto';
      inputRef.current.style.height = Math.min(inputRef.current.scrollHeight, 200) + 'px';
    }
  };

  // -------- Pin / unpin --------
  const togglePin = async () => {
    if (!conversationId) return;
    const next = !isPinned;
    setIsPinned(next);
    try {
      const res = await fetch(`/api/conversations/${conversationId}`, {
        method: 'PATCH',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ pinned: next }),
      });
      if (!res.ok) {
        setIsPinned(!next); // revert on server error
        const err = await res.json().catch(() => ({}));
        console.error('Pin toggle failed:', res.status, err);
        return;
      }
      onPinnedChange?.();
    } catch (e) {
      setIsPinned(!next);
      console.error('Pin toggle threw:', e);
    }
  };


  // -------- Edit / Regenerate --------
  // Edit a user message: replace text in place, truncate everything after,
  // and re-send via /api/chat. Server history will retain the original
  // turn — this is local-state replay (good enough for v2 v1.0).
  const editMessage = (index) => {
    const msg = messages[index];
    if (!msg || msg.role !== 'user') return;
    const next = window.prompt('Edit your message:', msg.content);
    if (next === null) return;
    const trimmed = next.trim();
    if (!trimmed) return;
    setMessages(prev => prev.slice(0, index));
    sendMessageWithText(trimmed, msg.images, msg.docs);
  };
  const regenerateMessage = (index) => {
    const assistantMsg = messages[index];
    if (!assistantMsg || assistantMsg.role !== 'assistant') return;
    // Find the user message that prompted this response
    let userIdx = index - 1;
    while (userIdx >= 0 && messages[userIdx].role !== 'user') userIdx--;
    if (userIdx < 0) return;
    const userMsg = messages[userIdx];
    setMessages(prev => prev.slice(0, index));
    sendMessageWithText(userMsg.content, userMsg.images, userMsg.docs);
  };

  // -------- Attachments (image + PDF) --------
  const readFileAsBase64 = (file) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const result = reader.result;
      const base64 = (result || '').split(',')[1] || '';
      resolve(base64);
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
  const handleAttachClick = () => fileInputRef.current?.click();
  const handleFileChosen = async (e) => {
    setAttachError('');
    const files = Array.from(e.target.files || []);
    e.target.value = '';
    const ALLOWED_IMG = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    const MAX_IMG = 5 * 1024 * 1024;
    const MAX_PDF = 32 * 1024 * 1024;
    const newImages = [];
    const newDocs = [];
    for (const file of files) {
      try {
        if (ALLOWED_IMG.includes(file.type)) {
          if (file.size > MAX_IMG) { setAttachError(`${file.name} is too big (max 5MB).`); continue; }
          const base64 = await readFileAsBase64(file);
          newImages.push({ base64, mediaType: file.type, preview: `data:${file.type};base64,${base64}` });
        } else if (file.type === 'application/pdf') {
          if (file.size > MAX_PDF) { setAttachError(`${file.name} is too big (max 32MB).`); continue; }
          const base64 = await readFileAsBase64(file);
          newDocs.push({ base64, mediaType: file.type, name: file.name, size: file.size });
        } else {
          setAttachError('Only JPG, PNG, GIF, WebP, and PDF files are supported.');
        }
      } catch {
        setAttachError(`Could not read ${file.name}.`);
      }
    }
    if (newImages.length > 0) {
      setPendingImages(prev => {
        const merged = [...prev, ...newImages];
        if (merged.length > 3) { setAttachError('Maximum 3 images per message.'); return merged.slice(0, 3); }
        return merged;
      });
    }
    if (newDocs.length > 0) {
      setPendingDocs(prev => {
        const merged = [...prev, ...newDocs];
        if (merged.length > 3) { setAttachError('Maximum 3 PDFs per message.'); return merged.slice(0, 3); }
        return merged;
      });
    }
  };
  const removeImage = (i) => setPendingImages(prev => prev.filter((_, idx) => idx !== i));
  const removeDoc = (i) => setPendingDocs(prev => prev.filter((_, idx) => idx !== i));

  const sendMessage = async () => {
    const text = input.trim();
    if ((!text && pendingImages.length === 0 && pendingDocs.length === 0) || isThinking) return;

    const imagesToSend = [...pendingImages];
    const docsToSend = [...pendingDocs];

    setInput('');
    if (inputRef.current) inputRef.current.style.height = 'auto';
    setPendingImages([]);
    setPendingDocs([]);
    setAttachError('');

    await sendMessageWithText(text, imagesToSend.length ? imagesToSend.map(i => i.preview) : null, docsToSend.length ? docsToSend.map(d => ({ name: d.name, size: d.size })) : null, imagesToSend, docsToSend);
  };

  // Shared send routine — used by sendMessage, editMessage, regenerateMessage.
  // For edit/regenerate, no fresh attachments (uses preserved local state).
  const sendMessageWithText = async (text, imagePreviews, docMeta, freshImages, freshDocs) => {
    if (isThinking) return;

    const userMsg = { role: 'user', content: text };
    if (imagePreviews && imagePreviews.length > 0) userMsg.images = imagePreviews;
    if (docMeta && docMeta.length > 0) userMsg.docs = docMeta;

    setMessages(prev => [...prev, userMsg]);
    setIsThinking(true);

    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          pluginId: plugin.id,
          conversationId,
          message: text || '(see attachments)',
          ...(freshImages && freshImages.length > 0 ? { images: freshImages.map(img => ({ base64: img.base64, mediaType: img.mediaType })) } : {}),
          ...(freshDocs && freshDocs.length > 0 ? { documents: freshDocs.map(doc => ({ base64: doc.base64, mediaType: doc.mediaType, name: doc.name })) } : {}),
        }),
      });

      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        setMessages(prev => [...prev, { role: 'assistant', content: err.error || 'Something went wrong. Try again.' }]);
        setIsThinking(false);
        return;
      }

      const newConvoId = res.headers.get('X-Conversation-Id');
      if (newConvoId) {
        // Mark as locally-owned BEFORE bubbling up — the parent will echo this
        // back as initialConversationId and trigger the load effect; the guard
        // there checks this set and skips the racing fetch.
        locallyOwnedConvoIds.current.add(newConvoId);
        setConversationId(newConvoId);
        onConversationChange?.(newConvoId);
      }
      const wasCompacted = res.headers.get('X-Compacted') === 'true';
      if (wasCompacted) {
        setMessages(prev => [...prev, { role: 'compaction', content: 'Conversation summarized to keep things fast.' }]);
      }

      // Stream SSE response
      setIsThinking(false);
      setMessages(prev => [...prev, { role: 'assistant', content: '' }]);

      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let assistantContent = '';
      let sseBuffer = '';

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        const chunk = decoder.decode(value, { stream: true });
        sseBuffer += chunk;
        const lines = sseBuffer.split('\n');
        sseBuffer = lines.pop() || '';
        for (const line of lines) {
          if (!line.startsWith('data: ')) continue;
          const data = line.slice(6);
          if (data === '[DONE]') continue;
          try {
            const event = JSON.parse(data);
            if (event.type === 'content_block_delta' && event.delta?.text) {
              assistantContent += event.delta.text;
              setMessages(prev => {
                const next = [...prev];
                next[next.length - 1] = { role: 'assistant', content: assistantContent };
                return next;
              });
            }
          } catch {}
        }
      }
    } catch {
      setIsThinking(false);
      setMessages(prev => [...prev, { role: 'assistant', content: 'Could not connect. Try again.' }]);
    }
  };

  const handleKeyDown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  };

  return (
    <div className="chat-screen">
      <header className="chat-header">
        <button
          className="chat-header-back"
          onClick={onOpenHistory}
          aria-label="Open conversation history"
          title="Past conversations"
        >
          <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="12" cy="12" r="10"/>
            <polyline points="12 6 12 12 16 14"/>
          </svg>
        </button>
        <div className="avatar avatar--md">
          <PluginAvatarLayers visuals={visuals} />
        </div>
        <div className="chat-header-meta">
          <h2 className="chat-header-name">{firstName}</h2>
          <p className="chat-header-role">{role}</p>
        </div>
        {tagLabel && (
          <span className={`badge ${tagLower === 'messaging' ? 'badge--messaging' : 'badge--strategy'}`}>
            {tagLabel}
          </span>
        )}
        <div className="chat-header-actions">
          {conversationId && (
            <button
              className={`chat-icon-btn ${isPinned ? 'chat-icon-btn--active' : ''}`}
              onClick={togglePin}
              title={isPinned ? 'Unpin' : 'Pin'}
              aria-label={isPinned ? 'Unpin conversation' : 'Pin conversation'}
            >
              <svg viewBox="0 0 24 24" fill={isPinned ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                <line x1="12" y1="17" x2="12" y2="22"/>
                <path d="M5 17h14V11l-2-2V4H7v5L5 11v6z"/>
              </svg>
            </button>
          )}
        </div>
      </header>

      <div className="chat-messages">
        {messages.map((m, i) => (
          <ChatMessage
            key={i}
            role={m.role}
            content={m.content}
            visuals={visuals}
            images={m.images}
            docs={m.docs}
            onEdit={m.role === 'user' && !isThinking ? () => editMessage(i) : undefined}
            onRegenerate={m.role === 'assistant' && !isThinking && i > 0 ? () => regenerateMessage(i) : undefined}
          />
        ))}
        {isThinking && <ThinkingMessage visuals={visuals} />}
        <div ref={messagesEndRef} />
      </div>

      <footer className="chat-composer">
        {(pendingImages.length > 0 || pendingDocs.length > 0 || attachError) && (
          <div className="chat-attachments">
            {pendingImages.map((img, i) => (
              <div key={`img-${i}`} className="chat-attachment">
                <img src={img.preview} alt="" />
                <span className="chat-attachment-name">image</span>
                <button className="chat-attachment-remove" onClick={() => removeImage(i)} aria-label="Remove">×</button>
              </div>
            ))}
            {pendingDocs.map((doc, i) => (
              <div key={`doc-${i}`} className="chat-attachment">
                <span>📄</span>
                <span className="chat-attachment-name">{doc.name}</span>
                <button className="chat-attachment-remove" onClick={() => removeDoc(i)} aria-label="Remove">×</button>
              </div>
            ))}
            {attachError && <span className="chat-attach-error">{attachError}</span>}
          </div>
        )}
        <div className="chat-composer-row">
          <button className="chat-attach-btn" onClick={handleAttachClick} aria-label="Attach files">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
              <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
            </svg>
          </button>
          <input
            ref={fileInputRef}
            type="file"
            accept="image/jpeg,image/png,image/gif,image/webp,application/pdf"
            multiple
            onChange={handleFileChosen}
            style={{ display: 'none' }}
          />
          <textarea
            ref={inputRef}
            className="chat-input"
            placeholder={`Message ${firstName}…`}
            value={input}
            onChange={handleInputChange}
            onKeyDown={handleKeyDown}
            disabled={isThinking}
            rows={1}
          />
          <button
            className="chat-send"
            onClick={sendMessage}
            disabled={(!input.trim() && pendingImages.length === 0 && pendingDocs.length === 0) || isThinking}
          >
            Send
          </button>
        </div>
      </footer>
    </div>
  );
}

// =====================================================================
// TrainingContent — modules → lessons → HTML lesson view.
// Read-only for v2 v1.0; curriculum editing still happens outside this view.
// Uses /api/curriculum/* (same backend as v1).
// =====================================================================
function TrainingContent({ user, activeLessonId, onLessonChange }) {
  const [modules, setModules] = useState(null);
  const [activeLesson, setActiveLesson] = useState(null);
  const [loadingLessonId, setLoadingLessonId] = useState(null);
  const [error, setError] = useState(null);
  const isAdmin = user?.role === 'admin';

  useEffect(() => {
    fetch('/api/curriculum/modules', { credentials: 'include' })
      .then(r => r.json())
      .then(data => setModules(data.modules || []))
      .catch(() => { setModules([]); setError('Could not load training modules.'); });
  }, []);

  // Sync URL lesson id → activeLesson detail. When the URL points at a
  // lesson we don't have loaded yet, fetch it.
  useEffect(() => {
    if (!activeLessonId) {
      if (activeLesson) setActiveLesson(null);
      return;
    }
    if (activeLesson?.id === activeLessonId) return;
    if (!modules) return; // wait for module list so we can resolve module_id
    let lessonStub = null;
    for (const m of modules) {
      const found = (m.lessons || []).find(l => l.id === activeLessonId);
      if (found) { lessonStub = found; break; }
    }
    if (!lessonStub) return; // unknown id; ignore
    const unlockIso = LESSON_UNLOCKS[lessonStub.title];
    if (!isLessonUnlockedFor(unlockIso, user, Date.now())) {
      onLessonChange?.(null);
      return;
    }
    setLoadingLessonId(activeLessonId);
    fetch(`/api/curriculum/modules/${lessonStub.module_id}/lessons/${activeLessonId}`, { credentials: 'include' })
      .then(r => r.json())
      .then(data => { if (data.lesson) setActiveLesson(data.lesson); })
      .catch(() => setError('Could not load lesson.'))
      .finally(() => setLoadingLessonId(null));
  }, [activeLessonId, modules, user]);

  const openLesson = (lesson) => {
    const unlockIso = LESSON_UNLOCKS[lesson.title];
    if (!isLessonUnlockedFor(unlockIso, user, Date.now())) return;
    onLessonChange?.(lesson.id);
  };
  const closeLesson = () => {
    onLessonChange?.(null);
  };

  // Lesson detail view
  if (activeLesson) {
    return (
      <div className="training-content">
        <div className="dashboard-greeting">
          <h1 className="hero-title">Watch, learn, do.</h1>
          <p className="hero-sub">The secrets of unicorn category creators, revealed.</p>
        </div>
        <div className="lesson-detail">
          <button className="lesson-back" onClick={closeLesson}>← Back to Training</button>
          <h2 className="lesson-title">{activeLesson.title}</h2>
          {activeLesson.content ? (
            <div className="lesson-body" dangerouslySetInnerHTML={{ __html: activeLesson.content }} />
          ) : (
            <p style={{ color: 'var(--c-ink-50)', fontStyle: 'italic' }}>Content coming soon.</p>
          )}
        </div>
      </div>
    );
  }

  // Modules list view
  const now = Date.now();
  return (
    <div className="training-content">
      <div className="dashboard-greeting">
        <h1 className="hero-title">Watch, learn, do.</h1>
        <p className="hero-sub">The secrets of unicorn category creators, revealed.</p>
      </div>

      {!modules ? (
        <div className="training-empty">Loading training modules…</div>
      ) : error ? (
        <div className="training-empty">{error}</div>
      ) : modules.length === 0 ? (
        <div className="training-empty">No training content available yet.</div>
      ) : (
        <div className="training-modules">
          {modules.map(mod => {
            // Hide unpublished modules from non-admins. The API already
            // filters them out for non-admins; admins see drafts too.
            if (mod.published === 0 && !isAdmin) return null;
            return (
              <div key={mod.id} className="module-block">
                <h2 className="module-title">
                  {mod.title}
                  {!mod.published && isAdmin && <span className="draft-badge">Draft</span>}
                </h2>
                <div className="module-lessons">
                  {mod.lessons.length === 0 ? (
                    <div className="module-empty">No lessons yet.</div>
                  ) : (
                    mod.lessons.map(lesson => {
                      if (lesson.published === 0 && !isAdmin) return null;
                      const loading = loadingLessonId === lesson.id;
                      const unlockIso = LESSON_UNLOCKS[lesson.title];
                      const locked = !isLessonUnlockedFor(unlockIso, user, now);
                      return (
                        <button
                          key={lesson.id}
                          className={`lesson-row${locked ? ' lesson-row--locked' : ''}`}
                          onClick={() => openLesson(lesson)}
                          disabled={loading || locked}
                          title={locked && unlockIso ? `Available ${formatUnlockDate(unlockIso)}` : undefined}
                          aria-disabled={locked || undefined}
                        >
                          <span className="lesson-row-title">
                            {loading ? 'Loading…' : lesson.title}
                          </span>
                          {!lesson.published && isAdmin && <span className="draft-badge">Draft</span>}
                          {locked && unlockIso ? (
                            <span className="lesson-row-lock"><LockIcon /> Available {formatUnlockDate(unlockIso)}</span>
                          ) : (
                            <span className="lesson-row-arrow">→</span>
                          )}
                        </button>
                      );
                    })
                  )}
                </div>
              </div>
            );
          })}
        </div>
      )}

    </div>
  );
}

// =====================================================================
// AdminContent — three tabs: Users, Plugins (link to v1), Analytics.
// User management is the highest-value admin task; deep plugin editing
// stays in v1 for now (system prompts, knowledge files).
// =====================================================================
function AdminContent({ user, plugins, onPluginsChange, tab: tabProp, onTabChange }) {
  const validTabs = ['users', 'plugins', 'analytics', 'errors', 'financial', 'system'];
  const tab = validTabs.includes(tabProp) ? tabProp : 'users';
  const setTab = (next) => onTabChange?.(next);
  return (
    <div className="admin-content">
      <div className="dashboard-greeting">
        <h1 className="hero-title">Admin</h1>
        <p className="hero-sub">Manage users, plugins, analytics, and system settings.</p>
      </div>
      <div className="admin-tabs">
        <button className={`admin-tab ${tab === 'users' ? 'admin-tab--active' : ''}`} onClick={() => setTab('users')}>Users</button>
        <button className={`admin-tab ${tab === 'plugins' ? 'admin-tab--active' : ''}`} onClick={() => setTab('plugins')}>Plugins</button>
        <button className={`admin-tab ${tab === 'analytics' ? 'admin-tab--active' : ''}`} onClick={() => setTab('analytics')}>Analytics</button>
        <button className={`admin-tab ${tab === 'errors' ? 'admin-tab--active' : ''}`} onClick={() => setTab('errors')}>Errors</button>
        <button className={`admin-tab ${tab === 'financial' ? 'admin-tab--active' : ''}`} onClick={() => setTab('financial')}>Financial</button>
        <button className={`admin-tab ${tab === 'system' ? 'admin-tab--active' : ''}`} onClick={() => setTab('system')}>System</button>
      </div>
      {tab === 'users' && <AdminUsers />}
      {tab === 'plugins' && <AdminPlugins plugins={plugins} onPluginsChange={onPluginsChange} />}
      {tab === 'analytics' && <AdminAnalytics />}
      {tab === 'errors' && <AdminErrors />}
      {tab === 'financial' && <AdminFinancial />}
      {tab === 'system' && <AdminSystem />}
    </div>
  );
}

function AdminUsers() {
  const [users, setUsers] = useState([]);
  const [total, setTotal] = useState(0);
  const [page, setPage] = useState(1);
  const [search, setSearch] = useState('');
  const [loading, setLoading] = useState(false);
  const [tempPwd, setTempPwd] = useState(null);
  const PAGE_SIZE = 50;

  const loadUsers = async () => {
    setLoading(true);
    try {
      const params = new URLSearchParams({ page: String(page), limit: String(PAGE_SIZE) });
      if (search) params.set('search', search);
      const res = await fetch(`/api/admin/users?${params}`, { credentials: 'include' });
      const data = await res.json();
      if (res.ok) { setUsers(data.users || []); setTotal(data.total || 0); }
    } catch {}
    setLoading(false);
  };

  useEffect(() => { loadUsers(); }, [page]);

  const updateUser = async (userId, updates) => {
    try {
      const res = await fetch(`/api/admin/users/${userId}`, {
        method: 'PATCH',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(updates),
      });
      const data = await res.json().catch(() => ({}));
      if (res.ok) {
        if (data.tempPassword) {
          const row = users.find(u => u.id === userId);
          setTempPwd({ email: row?.email || '', password: data.tempPassword });
        }
        loadUsers();
      }
    } catch {}
  };

  return (
    <div className="admin-card">
      {tempPwd && (
        <div className="admin-temp-pwd">
          Temporary password for <strong>{tempPwd.email}</strong>: <code>{tempPwd.password}</code>
          <button onClick={() => setTempPwd(null)} style={{ float: 'right', background: 'none', border: 'none', cursor: 'pointer', fontSize: 16, color: 'var(--c-ink)' }}>×</button>
        </div>
      )}
      <input
        className="admin-search"
        placeholder="Search users by email or name…"
        value={search}
        onChange={e => setSearch(e.target.value)}
        onKeyDown={e => { if (e.key === 'Enter') { setPage(1); loadUsers(); } }}
      />
      {loading ? (
        <div className="admin-empty">Loading…</div>
      ) : users.length === 0 ? (
        <div className="admin-empty">No users found.</div>
      ) : (
        <table className="admin-table">
          <thead>
            <tr>
              <th>Email</th>
              <th>Name</th>
              <th>Plan</th>
              <th>Messages</th>
              <th>Role</th>
              <th>Joined</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {users.map(u => (
              <tr key={u.id}>
                <td>{u.email}</td>
                <td className="admin-table-muted">{u.full_name || '—'}</td>
                <td>
                  <select
                    className="admin-plan-select"
                    value={u.role === 'admin' ? 'admin' : (u.plan || 'free')}
                    onChange={e => {
                      const v = e.target.value;
                      if (v === 'admin') updateUser(u.id, { role: 'admin' });
                      else updateUser(u.id, { role: 'user', plan: v });
                    }}
                  >
                    <option value="free">Free</option>
                    {((typeof STUDIO_CONFIG !== 'undefined' && STUDIO_CONFIG.plans) || []).map(p => (
                      <option key={p.id} value={p.id}>{p.name}</option>
                    ))}
                    <option value="admin">Admin</option>
                    {/* Render the user's current plan even if it's not in config (legacy / archived) */}
                    {u.role !== 'admin' && u.plan && u.plan !== 'free' && !((typeof STUDIO_CONFIG !== 'undefined' && STUDIO_CONFIG.plans) || []).some(p => p.id === u.plan) && (
                      <option value={u.plan}>{u.plan} (legacy)</option>
                    )}
                  </select>
                </td>
                <td className="admin-table-muted">{u.messages_used || 0}</td>
                <td className="admin-table-muted">{u.role || 'user'}</td>
                <td className="admin-table-muted">{u.created_at ? new Date(u.created_at).toLocaleDateString() : '—'}</td>
                <td>
                  <div className="admin-table-actions">
                    <button
                      className="admin-table-action"
                      onClick={() => { if (window.confirm(`Reset password for ${u.email}?`)) updateUser(u.id, { resetPassword: true }); }}
                    >Reset password</button>
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
      {total > PAGE_SIZE && (
        <div className="admin-pager">
          <span>{((page - 1) * PAGE_SIZE) + 1}–{Math.min(page * PAGE_SIZE, total)} of {total}</span>
          <div className="admin-pager-buttons">
            <button className="admin-table-action" disabled={page === 1} onClick={() => setPage(p => p - 1)}>Prev</button>
            <button className="admin-table-action" disabled={page * PAGE_SIZE >= total} onClick={() => setPage(p => p + 1)}>Next</button>
          </div>
        </div>
      )}
    </div>
  );
}

function AdminPlugins({ plugins, onPluginsChange }) {
  const [editing, setEditing] = useState(null); // plugin object being edited
  const [form, setForm] = useState({});
  const [systemPrompt, setSystemPrompt] = useState('');
  const [knowledge, setKnowledge] = useState('');
  const [loading, setLoading] = useState(false);
  const [saving, setSaving] = useState(false);
  const [status, setStatus] = useState(null);

  const openEditor = async (plugin) => {
    setEditing(plugin);
    setStatus(null);
    setLoading(true);
    setForm({
      name: plugin.name || '',
      emoji: plugin.emoji || '',
      description: plugin.description || '',
      greeting: plugin.greeting || '',
      role: plugin.role || '',
      color: plugin.color || '#6366f1',
      tag: plugin.tag || '',
      model: plugin.model || 'claude-sonnet-4-6',
      maxTokens: plugin.maxTokens || 4000,
    });
    setSystemPrompt('');
    setKnowledge('');
    try {
      const res = await fetch(`/api/admin/plugins/${plugin.id}`, { credentials: 'include' });
      if (res.ok) {
        const data = await res.json();
        if (data.override) {
          setForm(prev => ({ ...prev, ...data.override }));
        }
        if (data.systemPrompt !== undefined) setSystemPrompt(data.systemPrompt || '');
        if (data.knowledge !== undefined) setKnowledge(data.knowledge || '');
      }
    } catch {}
    setLoading(false);
  };

  const closeEditor = () => { setEditing(null); setStatus(null); };

  const knowledgeTokens = Math.round(knowledge.length / 4);
  const knowledgeBlocked = knowledgeTokens >= 16000;

  const savePlugin = async () => {
    if (!editing) return;
    if (knowledgeBlocked) {
      setStatus({ kind: 'error', message: 'Knowledge file is too large (>16k tokens). Trim it before saving.' });
      return;
    }
    setSaving(true);
    setStatus(null);
    try {
      const res = await fetch(`/api/admin/plugins/${editing.id}`, {
        method: 'PUT',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ override: form, systemPrompt, knowledge: knowledge || null }),
      });
      if (res.ok) {
        setStatus({ kind: 'success', message: 'Plugin saved.' });
        onPluginsChange?.();
      } else {
        const d = await res.json().catch(() => ({}));
        setStatus({ kind: 'error', message: d.error || 'Save failed.' });
      }
    } catch {
      setStatus({ kind: 'error', message: 'Could not connect.' });
    }
    setSaving(false);
  };

  const resetToDefault = async () => {
    if (!editing) return;
    if (!window.confirm(`Reset ${editing.name} to default? Your overrides will be lost.`)) return;
    setSaving(true);
    setStatus(null);
    try {
      const res = await fetch(`/api/admin/plugins/${editing.id}`, {
        method: 'DELETE',
        credentials: 'include',
      });
      if (res.ok) {
        setStatus({ kind: 'success', message: 'Reset to default.' });
        onPluginsChange?.();
        closeEditor();
      } else {
        setStatus({ kind: 'error', message: 'Reset failed.' });
      }
    } catch {
      setStatus({ kind: 'error', message: 'Could not connect.' });
    }
    setSaving(false);
  };

  if (editing) {
    return (
      <div className="admin-card">
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20, gap: 12, flexWrap: 'wrap' }}>
          <button className="lesson-back" onClick={closeEditor}>← Back to plugins</button>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            {status && <span className={`settings-status settings-status--${status.kind}`}>{status.message}</span>}
            <button className="admin-table-action admin-table-action--danger" onClick={resetToDefault} disabled={saving}>Reset to default</button>
            <button className="settings-action-btn" onClick={savePlugin} disabled={saving || loading || knowledgeBlocked}>
              {saving ? 'Saving…' : 'Save changes'}
            </button>
          </div>
        </div>

        {loading ? (
          <div className="admin-empty">Loading plugin…</div>
        ) : (
          <div className="settings-field-group">
            <label className="settings-field">
              <span className="settings-label">Name</span>
              <input className="settings-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
            </label>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
              <label className="settings-field">
                <span className="settings-label">Emoji</span>
                <input className="settings-input" value={form.emoji} onChange={e => setForm({ ...form, emoji: e.target.value })} maxLength={4} />
              </label>
              <label className="settings-field">
                <span className="settings-label">Tag</span>
                <input className="settings-input" value={form.tag} onChange={e => setForm({ ...form, tag: e.target.value })} placeholder="e.g. Strategy" />
              </label>
            </div>
            <label className="settings-field">
              <span className="settings-label">Role</span>
              <input className="settings-input" value={form.role} onChange={e => setForm({ ...form, role: e.target.value })} placeholder="e.g. The AI Category POV Writer" />
            </label>
            <label className="settings-field">
              <span className="settings-label">Description</span>
              <textarea
                className="settings-input"
                style={{ height: 'auto', minHeight: 64, padding: '12px 16px', resize: 'vertical' }}
                value={form.description}
                onChange={e => setForm({ ...form, description: e.target.value })}
                rows={2}
              />
            </label>
            <label className="settings-field">
              <span className="settings-label">Greeting</span>
              <textarea
                className="settings-input"
                style={{ height: 'auto', minHeight: 64, padding: '12px 16px', resize: 'vertical' }}
                value={form.greeting}
                onChange={e => setForm({ ...form, greeting: e.target.value })}
                rows={2}
              />
            </label>
            <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 14 }}>
              <label className="settings-field">
                <span className="settings-label">Model</span>
                <select className="admin-plan-select" style={{ height: 48 }} value={form.model} onChange={e => setForm({ ...form, model: e.target.value })}>
                  <option value="claude-haiku-4-5-20251001">Claude Haiku 4.5 (fast/cheap)</option>
                  <option value="claude-sonnet-4-6">Claude Sonnet 4.6 (default)</option>
                  <option value="claude-opus-4-6">Claude Opus 4.6 (deep reasoning)</option>
                </select>
              </label>
              <label className="settings-field">
                <span className="settings-label">Max tokens</span>
                <input className="settings-input" type="number" min={500} max={16000} step={100} value={form.maxTokens} onChange={e => setForm({ ...form, maxTokens: parseInt(e.target.value, 10) || 4000 })} />
              </label>
            </div>
            <label className="settings-field">
              <span className="settings-label">System prompt</span>
              <textarea
                className="settings-input"
                style={{ height: 'auto', minHeight: 320, padding: '12px 16px', resize: 'vertical', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 13, lineHeight: 1.5 }}
                value={systemPrompt}
                onChange={e => setSystemPrompt(e.target.value)}
                rows={20}
              />
              <span className="settings-helper">
                The instructions that shape how this plugin behaves.
              </span>
            </label>
            <label className="settings-field">
              <span className="settings-label">Knowledge file</span>
              <textarea
                className="settings-input"
                style={{ height: 'auto', minHeight: 240, padding: '12px 16px', resize: 'vertical', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 13, lineHeight: 1.5 }}
                value={knowledge}
                onChange={e => setKnowledge(e.target.value)}
                rows={14}
                placeholder="Reference material the assistant can draw on — frameworks, key concepts, processes, terminology, etc. Leave empty if this assistant doesn't need a knowledge base."
              />
              <span className="settings-helper">
                ~{knowledgeTokens.toLocaleString()} tokens
                {knowledgeTokens >= 10000 && knowledgeTokens < 16000 && (
                  <span style={{ color: '#d97706' }}> — Large knowledge base, this will increase API cost per message</span>
                )}
                {knowledgeBlocked && (
                  <span style={{ color: '#dc2626' }}> — Too large (≥16k tokens). Trim before saving.</span>
                )}
              </span>
            </label>
          </div>
        )}
      </div>
    );
  }

  return (
    <div className="admin-card">
      <div style={{ marginBottom: 14 }}>
        <span style={{ fontSize: 14, color: 'var(--c-ink-50)' }}>
          {(plugins || []).length} plugin{plugins?.length === 1 ? '' : 's'}. Click any to edit name, emoji, description, system prompt, knowledge file, and model.
        </span>
      </div>
      {(plugins || []).length === 0 ? (
        <div className="admin-empty">No plugins configured.</div>
      ) : (
        <table className="admin-table">
          <thead>
            <tr>
              <th>Plugin</th>
              <th>Tag</th>
              <th>Model</th>
              <th>Max tokens</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {plugins.map(p => (
              <tr key={p.id}>
                <td>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <span style={{ fontSize: 18 }}>{p.emoji || '🔧'}</span>
                    <span>{p.name}</span>
                  </div>
                </td>
                <td className="admin-table-muted">{p.tag || '—'}</td>
                <td className="admin-table-muted">{p.model || '—'}</td>
                <td className="admin-table-muted">{p.maxTokens || '—'}</td>
                <td>
                  <button className="admin-table-action" onClick={() => openEditor(p)}>Edit</button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

function AdminErrors() {
  const [errors, setErrors] = useState(null);
  const [days] = useState(30);
  useEffect(() => {
    (async () => {
      try {
        const res = await fetch(`/api/admin/errors?days=${days}`, { credentials: 'include' });
        if (res.ok) {
          const data = await res.json();
          setErrors(data.errors || []);
        } else { setErrors([]); }
      } catch { setErrors([]); }
    })();
  }, [days]);
  if (errors === null) return <div className="admin-card"><div className="admin-empty">Loading…</div></div>;
  return (
    <div className="admin-card">
      <div style={{ marginBottom: 12, fontSize: 14, color: 'var(--c-ink-50)' }}>Errors logged in the last {days} days.</div>
      {errors.length === 0 ? (
        <div className="admin-empty">No errors recorded. 🎉</div>
      ) : (
        <table className="admin-table">
          <thead>
            <tr>
              <th>When</th>
              <th>Type</th>
              <th>Message</th>
              <th>User</th>
            </tr>
          </thead>
          <tbody>
            {errors.map((e, i) => (
              <tr key={e.id || i}>
                <td className="admin-table-muted">{e.created_at ? new Date(e.created_at).toLocaleString() : '—'}</td>
                <td>{e.error_type || e.type || '—'}</td>
                <td>{e.message || e.error_message || '—'}</td>
                <td className="admin-table-muted">{e.user_email || e.user_id || '—'}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

function AdminFinancial() {
  const [revenue, setRevenue] = useState(null);
  const [days] = useState(30);
  useEffect(() => {
    (async () => {
      try {
        const res = await fetch(`/api/admin/revenue?days=${days}`, { credentials: 'include' });
        const data = await res.json();
        if (res.ok && !data.error) setRevenue(data);
        else setRevenue({ error: data.error || 'Could not load revenue data.' });
      } catch { setRevenue({ error: 'Could not connect to payment provider.' }); }
    })();
  }, [days]);
  if (!revenue) return <div className="admin-card"><div className="admin-empty">Loading…</div></div>;
  if (revenue.error) return <div className="admin-card"><div className="admin-empty">{revenue.error}</div></div>;

  const fmt = (n) => '$' + ((n || 0) / 100).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  const items = [
    { icon: 'dollar', label: 'MRR',                value: fmt(revenue.mrr) },
    { icon: 'dollar', label: `Revenue (${days}d)`, value: fmt(revenue.recentRevenue || revenue.totalRevenue) },
    { icon: 'users',  label: 'Active Subs',        value: String(revenue.activeSubscriptions || 0) },
    { icon: 'flash',  label: 'Customers',          value: String(revenue.totalCustomers || 0) },
  ];
  return (
    <div className="stat-grid stat-grid--compact">
      {items.map(s => <StatCard key={s.label} icon={s.icon} value={s.value} label={s.label} />)}
    </div>
  );
}

function AdminSystem() {
  const [rateLimits, setRateLimits] = useState(null);
  const [rateStats, setRateStats] = useState(null);
  const [tier, setTier] = useState(null);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    (async () => {
      try { const r = await fetch('/api/admin/ratelimits', { credentials: 'include' }); if (r.ok) setRateLimits(await r.json()); } catch {}
      try { const r = await fetch('/api/admin/ratestats?days=7', { credentials: 'include' }); if (r.ok) setRateStats(await r.json()); } catch {}
      try { const r = await fetch('/api/admin/tier', { credentials: 'include' }); if (r.ok) { const d = await r.json(); setTier(d.tier); } } catch {}
    })();
  }, []);

  const saveTier = async (newTier) => {
    setSaving(true);
    setTier(newTier);
    try {
      await fetch('/api/admin/tier', {
        method: 'PUT',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ tier: newTier }),
      });
      const r = await fetch('/api/admin/ratelimits', { credentials: 'include' });
      if (r.ok) setRateLimits(await r.json());
    } catch {}
    setSaving(false);
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      <div className="admin-card">
        <h3 style={{ fontFamily: 'var(--ft-text)', fontWeight: 500, fontSize: 16, color: 'var(--c-ink)', marginBottom: 12 }}>Anthropic Tier Override</h3>
        <p style={{ fontSize: 13, color: 'var(--c-ink-50)', marginBottom: 14, lineHeight: 1.5 }}>
          Sets the Anthropic API tier this studio uses for rate-limit calculations. Higher tier = larger spend cap and TPM limits.
        </p>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
          {[1, 2, 3, 4].map(t => (
            <button
              key={t}
              className={`admin-table-action ${tier === t ? '' : ''}`}
              style={{ background: tier === t ? 'var(--c-pink-10)' : 'transparent', borderColor: tier === t ? 'var(--c-pink)' : 'var(--c-border)', color: tier === t ? 'var(--c-pink)' : 'var(--c-ink)' }}
              disabled={saving}
              onClick={() => saveTier(t)}
            >Tier {t}</button>
          ))}
          <button
            className="admin-table-action"
            disabled={saving}
            onClick={() => saveTier(null)}
          >Auto</button>
        </div>
      </div>

      <div className="admin-card">
        <h3 style={{ fontFamily: 'var(--ft-text)', fontWeight: 500, fontSize: 16, color: 'var(--c-ink)', marginBottom: 12 }}>Current Rate Limits</h3>
        {!rateLimits ? (
          <div className="admin-empty">Loading…</div>
        ) : (
          <table className="admin-table">
            <tbody>
              <tr><td>Daily spend cap</td><td className="admin-table-muted">${(rateLimits.dailyCap || 0).toFixed(2)}</td></tr>
              <tr><td>Spent today</td><td className="admin-table-muted">${(rateLimits.spentToday || 0).toFixed(2)}</td></tr>
              <tr><td>Requests / min</td><td className="admin-table-muted">{rateLimits.rpm || '—'}</td></tr>
              <tr><td>Input TPM</td><td className="admin-table-muted">{(rateLimits.inputTPM || 0).toLocaleString()}</td></tr>
              <tr><td>Output TPM</td><td className="admin-table-muted">{(rateLimits.outputTPM || 0).toLocaleString()}</td></tr>
            </tbody>
          </table>
        )}
      </div>

      {rateStats && (
        <div className="admin-card">
          <h3 style={{ fontFamily: 'var(--ft-text)', fontWeight: 500, fontSize: 16, color: 'var(--c-ink)', marginBottom: 12 }}>Rate Stats — Last 7 Days</h3>
          <table className="admin-table">
            <tbody>
              <tr><td>Rate-limit hits</td><td className="admin-table-muted">{rateStats.rateLimitHits || 0}</td></tr>
              <tr><td>Spend-cap hits</td><td className="admin-table-muted">{rateStats.spendCapHits || 0}</td></tr>
              <tr><td>Total requests</td><td className="admin-table-muted">{(rateStats.totalRequests || 0).toLocaleString()}</td></tr>
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

function AdminAnalytics() {
  const [stats, setStats] = useState(null);
  const [days] = useState(30);

  useEffect(() => {
    (async () => {
      try {
        const res = await fetch(`/api/admin/usage?days=${days}`, { credentials: 'include' });
        if (res.ok) setStats(await res.json());
      } catch {}
    })();
  }, [days]);

  if (!stats) return <div className="admin-card"><div className="admin-empty">Loading…</div></div>;

  const items = [
    { icon: 'users', label: 'Total Users', value: String(stats.totalUsers || 0) },
    { icon: 'signal', label: `Active (${days}d)`, value: String(stats.activeUsers || 0) },
    { icon: 'chat-bubble', label: `Messages (${days}d)`, value: (stats.totals?.total_messages || 0).toLocaleString() },
    { icon: 'dollar', label: 'Est. API Cost', value: '$' + computeApiCost(stats) },
  ];

  return (
    <div className="stat-grid">
      {items.map(s => (
        <StatCard key={s.label} icon={s.icon} value={s.value} label={s.label} />
      ))}
    </div>
  );
}

// =====================================================================
// SettingsContent — profile / password / subscription / support.
// Mirrors v1's SettingsPage IA. Uses the same backend endpoints.
// =====================================================================
function SettingsContent({ user, onUpdate, onNav }) {
  const [fullName, setFullName] = useState(user?.full_name || '');
  const [displayName, setDisplayName] = useState(user?.display_name || '');
  const [currentPass, setCurrentPass] = useState('');
  const [newPass, setNewPass] = useState('');
  const [newEmail, setNewEmail] = useState('');
  const [showEmailChange, setShowEmailChange] = useState(false);

  const [profileStatus, setProfileStatus] = useState(null);
  const [passwordStatus, setPasswordStatus] = useState(null);
  const [emailStatus, setEmailStatus] = useState(null);

  const [savingProfile, setSavingProfile] = useState(false);
  const [savingPassword, setSavingPassword] = useState(false);
  const [savingEmail, setSavingEmail] = useState(false);

  const cfg = (typeof STUDIO_CONFIG !== 'undefined') ? STUDIO_CONFIG : {};
  const planLimits = { free: cfg.rateLimits?.freeTrialMessages || 10 };
  (cfg.plans || []).forEach(p => { planLimits[p.id] = p.monthlyMessageLimit; });
  const limit = planLimits[user?.plan] || 10;
  const used = user?.messages_used || 0;
  const pct = Math.min((used / limit) * 100, 100);
  const progressClass = pct > 90 ? 'settings-progress--danger' : pct > 70 ? 'settings-progress--warning' : '';

  const handleSaveProfile = async () => {
    setSavingProfile(true);
    setProfileStatus(null);
    try {
      const res = await fetch('/api/auth/me', {
        method: 'PATCH',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ fullName: fullName.trim(), displayName: displayName.trim() || null }),
      });
      if (res.ok) {
        setProfileStatus({ kind: 'success', message: 'Profile updated.' });
        onUpdate?.();
      } else {
        const d = await res.json().catch(() => ({}));
        setProfileStatus({ kind: 'error', message: d.error || 'Update failed.' });
      }
    } catch {
      setProfileStatus({ kind: 'error', message: 'Something went wrong.' });
    }
    setSavingProfile(false);
  };

  const handleChangePassword = async () => {
    if (!currentPass || !newPass || newPass.length < 8) {
      setPasswordStatus({ kind: 'error', message: 'New password must be at least 8 characters.' });
      return;
    }
    setSavingPassword(true);
    setPasswordStatus(null);
    try {
      const res = await fetch('/api/auth/me', {
        method: 'PATCH',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ currentPassword: currentPass, newPassword: newPass }),
      });
      if (res.ok) {
        setPasswordStatus({ kind: 'success', message: 'Password updated.' });
        setCurrentPass('');
        setNewPass('');
      } else {
        const d = await res.json().catch(() => ({}));
        setPasswordStatus({ kind: 'error', message: d.error || 'Update failed.' });
      }
    } catch {
      setPasswordStatus({ kind: 'error', message: 'Something went wrong.' });
    }
    setSavingPassword(false);
  };

  const handleChangeEmail = async () => {
    if (!newEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
      setEmailStatus({ kind: 'error', message: 'Please enter a valid email address.' });
      return;
    }
    if (newEmail.toLowerCase() === user?.email?.toLowerCase()) {
      setEmailStatus({ kind: 'error', message: "That's already your current email." });
      return;
    }
    setSavingEmail(true);
    setEmailStatus(null);
    try {
      const res = await fetch('/api/auth/change-email', {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ newEmail: newEmail.trim() }),
      });
      const d = await res.json().catch(() => ({}));
      if (res.ok) {
        setEmailStatus({ kind: 'success', message: 'Verification email sent. Check your new inbox.' });
        setShowEmailChange(false);
        setNewEmail('');
      } else {
        setEmailStatus({ kind: 'error', message: d.error || 'Failed to send verification email.' });
      }
    } catch {
      setEmailStatus({ kind: 'error', message: 'Something went wrong.' });
    }
    setSavingEmail(false);
  };

  const cfgForSettings = (typeof STUDIO_CONFIG !== 'undefined') ? STUDIO_CONFIG : {};
  const settingsCurrentPlan = (cfgForSettings.plans || []).find(p => p.id === user?.plan);
  const settingsNextPlan = (cfgForSettings.plans || []).find(p => p.price > (settingsCurrentPlan?.price || 0));
  const openBilling = () => openBillingPortal();
  const openUpgrade = () => onNav?.('pricing');

  return (
    <div className="settings-content">
      <div className="dashboard-greeting">
        <h1 className="hero-title">Settings</h1>
        <p className="hero-sub">Manage your profile, password, and subscription.</p>
      </div>

      {/* Profile */}
      <div className="settings-card">
        <h2 className="settings-card-title">Profile</h2>
        <div className="settings-field-group">
          <label className="settings-field">
            <span className="settings-label">Full name</span>
            <input
              className="settings-input"
              value={fullName}
              onChange={e => setFullName(e.target.value)}
              autoComplete="name"
            />
          </label>
          <label className="settings-field">
            <span className="settings-label">Display name</span>
            <input
              className="settings-input"
              placeholder="Shown on leaderboard (optional)"
              value={displayName}
              onChange={e => setDisplayName(e.target.value)}
            />
            <span className="settings-helper">How others see you on the leaderboard. Leave blank to use your first name + last initial.</span>
          </label>
          <div className="settings-field">
            <span className="settings-label">Email</span>
            <div className="settings-email-row">
              <input
                className="settings-input settings-input--readonly"
                readOnly
                value={user?.email || ''}
              />
              <button
                type="button"
                className="settings-email-change-btn"
                onClick={() => setShowEmailChange(s => !s)}
              >
                {showEmailChange ? 'Cancel' : 'Change'}
              </button>
            </div>
            {showEmailChange && (
              <div className="settings-email-change-form">
                <label className="settings-field">
                  <span className="settings-label">New email</span>
                  <input
                    className="settings-input"
                    type="email"
                    placeholder="Enter new email address"
                    value={newEmail}
                    onChange={e => setNewEmail(e.target.value)}
                  />
                </label>
                <div className="settings-action-row">
                  <button className="settings-action-btn" onClick={handleChangeEmail} disabled={savingEmail}>
                    {savingEmail ? 'Sending…' : 'Send verification'}
                  </button>
                  {emailStatus && (
                    <span className={`settings-status settings-status--${emailStatus.kind}`}>{emailStatus.message}</span>
                  )}
                </div>
              </div>
            )}
          </div>
        </div>
        <div className="settings-action-row">
          <button className="settings-action-btn" onClick={handleSaveProfile} disabled={savingProfile}>
            {savingProfile ? 'Saving…' : 'Save changes'}
          </button>
          {profileStatus && (
            <span className={`settings-status settings-status--${profileStatus.kind}`}>{profileStatus.message}</span>
          )}
        </div>
      </div>

      {/* Password */}
      <div className="settings-card">
        <h2 className="settings-card-title">Change password</h2>
        <div className="settings-field-group">
          <label className="settings-field">
            <span className="settings-label">Current password</span>
            <input
              className="settings-input"
              type="password"
              autoComplete="current-password"
              value={currentPass}
              onChange={e => setCurrentPass(e.target.value)}
            />
          </label>
          <label className="settings-field">
            <span className="settings-label">New password</span>
            <input
              className="settings-input"
              type="password"
              autoComplete="new-password"
              placeholder="Min. 8 characters"
              value={newPass}
              onChange={e => setNewPass(e.target.value)}
            />
          </label>
        </div>
        <div className="settings-action-row">
          <button className="settings-action-btn" onClick={handleChangePassword} disabled={savingPassword}>
            {savingPassword ? 'Updating…' : 'Update password'}
          </button>
          {passwordStatus && (
            <span className={`settings-status settings-status--${passwordStatus.kind}`}>{passwordStatus.message}</span>
          )}
        </div>
      </div>

      {/* Subscription — hidden for admins */}
      {user?.role !== 'admin' && (
        <div className="settings-card">
          <h2 className="settings-card-title">Subscription</h2>
          <div className="settings-row">
            <span className="settings-row-key">Plan</span>
            <span className="settings-row-value settings-row-value--capitalize">{user?.plan || 'free'}</span>
          </div>
          {user?.plan_grant?.expires_at && !user.plan_grant.expired && (
            <div className="settings-row">
              <span className="settings-row-key">Studio access ends</span>
              <span className="settings-row-value">{new Date(user.plan_grant.expires_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'America/Los_Angeles' })}</span>
            </div>
          )}
          <div className="settings-row">
            <span className="settings-row-key">Messages used</span>
            <span className="settings-row-value">{used} / {limit}</span>
          </div>
          <div className={`settings-progress ${progressClass}`}>
            <div style={{ width: `${pct}%` }} />
          </div>
          <div className="settings-action-row">
            {user?.plan && user.plan !== 'free' && user?.plan_grant?.billing_managed !== false ? (
              <button className="settings-action-btn settings-action-btn--ghost" onClick={openBilling}>
                Manage billing
              </button>
            ) : (
              <button className="settings-action-btn" onClick={openUpgrade}>
                Upgrade plan
              </button>
            )}
          </div>
        </div>
      )}

      {/* Support */}
      {cfg.support?.enabled && (
        <div className="settings-card">
          <h2 className="settings-card-title">Support</h2>
          <p className="settings-helper" style={{ marginBottom: 16 }}>Need help? Submit a support request.</p>
          <a
            className="settings-action-btn settings-action-btn--ghost"
            href={cfg.support.url}
            target="_blank"
            rel="noopener noreferrer"
            style={{ textDecoration: 'none' }}
          >
            Contact support
          </a>
        </div>
      )}
    </div>
  );
}

function PlaceholderContent({ title, message, ctaLabel, ctaHref }) {
  return (
    <>
      <header className="hero">
        <h1 className="hero-title">{title}</h1>
        <p className="hero-sub">{message}</p>
        {ctaHref && (
          <a className="hero-cta" href={ctaHref}>{ctaLabel || 'Open in v1 →'}</a>
        )}
      </header>
    </>
  );
}

// ---------------------------------------------------------------------
// Login screen
// ---------------------------------------------------------------------

// Reusable inline eye toggle SVG for password fields.
function EyeIcon({ shown }) {
  const props = { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' };
  return shown ? (
    <svg {...props}>
      <path d="M17.94 17.94A10.94 10.94 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A10.94 10.94 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
      <line x1="1" y1="1" x2="23" y2="23"/>
    </svg>
  ) : (
    <svg {...props}>
      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
      <circle cx="12" cy="12" r="3"/>
    </svg>
  );
}

function LoginScreen({ onAuth, onNavigate }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [showPwd, setShowPwd] = useState(false);
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (loading) return;
    setError('');
    setLoading(true);
    try {
      const res = await fetch('/api/auth/login', {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: email.trim(), password }),
      });
      if (!res.ok) {
        const data = await res.json().catch(() => ({}));
        setError(data.error || 'Could not sign in. Check your email and password.');
        setLoading(false);
        return;
      }
      // Cookie is set; load user + plugins and transition to dashboard.
      onAuth();
    } catch {
      setError('Could not connect. Try again.');
      setLoading(false);
    }
  };

  return (
    <div className="auth-page">
      <img className="auth-landscape" src={`${ASSETS}/landscape-hero.png`} alt="" aria-hidden="true" />
      <div className="auth-card">
        <div className="auth-brand">OYC<span className="reg">®</span></div>

        <h1 className="auth-headline">
          Welcome back.<br />
          <span className="auth-headline-accent">Let's keep going.</span>
        </h1>
        <p className="auth-subhead">Stop competing. Start defining your category.</p>

        <form className="auth-form" onSubmit={handleSubmit}>
          <label className="auth-field">
            <span className="auth-label">Email</span>
            <input
              className="auth-input"
              type="email"
              required
              value={email}
              onChange={e => setEmail(e.target.value)}
              placeholder="you@example.com"
              autoComplete="email"
              autoFocus
            />
          </label>

          <label className="auth-field">
            <span className="auth-label">Password</span>
            <div className="auth-password-wrap">
              <input
                className="auth-input"
                type={showPwd ? 'text' : 'password'}
                required
                value={password}
                onChange={e => setPassword(e.target.value)}
                autoComplete="current-password"
              />
              <button
                type="button"
                className="auth-password-toggle"
                onClick={() => setShowPwd(s => !s)}
                aria-label={showPwd ? 'Hide password' : 'Show password'}
              >
                <EyeIcon shown={showPwd} />
              </button>
            </div>
          </label>

          <button type="button" className="auth-forgot" onClick={() => onNavigate('forgot')}>
            Forgot password?
          </button>

          {error && <p className="auth-error">{error}</p>}

          <button type="submit" className="auth-submit" disabled={loading}>
            {loading ? 'Signing in…' : 'Sign In'}
          </button>

          <p className="auth-foot">
            Don't have an account?{' '}
            <a href="#" onClick={(e) => { e.preventDefault(); onNavigate('signup'); }}>Sign up</a>
          </p>
        </form>
      </div>

      <button className="auth-pricing" onClick={() => onNavigate('pricing')} style={{ background: 'none', border: 'none', cursor: 'pointer' }}>
        View pricing →
      </button>
    </div>
  );
}

// =====================================================================
// SignupScreen / ForgotScreen / ResetScreen — same auth-card chrome
// as LoginScreen, hit /api/auth/{signup,forgot,reset}.
// =====================================================================

function SignupScreen({ onNavigate, onAuth }) {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [showPwd, setShowPwd] = useState(false);
  const [agreed, setAgreed] = useState(false);
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (loading) return;
    if (!name.trim() || !email.trim() || !password) return;
    if (!agreed) { setError('Please agree to the Terms of Service and Privacy Policy.'); return; }
    setError('');
    setLoading(true);
    try {
      const res = await fetch('/api/auth/signup', {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: email.trim(), password, fullName: name.trim() }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || 'Signup failed.');
        setLoading(false);
        return;
      }
      onAuth();
    } catch {
      setError('Could not connect. Try again.');
      setLoading(false);
    }
  };

  return (
    <div className="auth-page">
      <img className="auth-landscape" src={`${ASSETS}/landscape-hero.png`} alt="" aria-hidden="true" />
      <div className="auth-card">
        <div className="auth-brand">OYC<span className="reg">®</span></div>

        <h1 className="auth-headline">
          Stop competing.<br />
          <span className="auth-headline-accent">Start defining.</span>
        </h1>
        <p className="auth-subhead">Create your {APP_NAME} account.</p>

        <form className="auth-form" onSubmit={handleSubmit}>
          <label className="auth-field">
            <span className="auth-label">Full name</span>
            <input className="auth-input" type="text" required value={name} onChange={e => setName(e.target.value)} placeholder="Your name" autoComplete="name" autoFocus />
          </label>
          <label className="auth-field">
            <span className="auth-label">Email</span>
            <input className="auth-input" type="email" required value={email} onChange={e => setEmail(e.target.value)} placeholder="you@example.com" autoComplete="email" />
          </label>
          <label className="auth-field">
            <span className="auth-label">Password</span>
            <div className="auth-password-wrap">
              <input className="auth-input" type={showPwd ? 'text' : 'password'} required value={password} onChange={e => setPassword(e.target.value)} placeholder="Min. 8 characters" autoComplete="new-password" />
              <button type="button" className="auth-password-toggle" onClick={() => setShowPwd(s => !s)} aria-label={showPwd ? 'Hide password' : 'Show password'}>
                <EyeIcon shown={showPwd} />
              </button>
            </div>
          </label>

          <div className="auth-terms">
            <input type="checkbox" id="terms" checked={agreed} onChange={e => setAgreed(e.target.checked)} />
            <label htmlFor="terms">
              I agree to the <a href="/terms" target="_blank" rel="noopener">Terms of Service</a> and <a href="/privacy" target="_blank" rel="noopener">Privacy Policy</a>.
            </label>
          </div>

          {error && <p className="auth-error">{error}</p>}

          <button type="submit" className="auth-submit" disabled={loading || !agreed}>
            {loading ? 'Creating account…' : 'Create account'}
          </button>

          <p className="auth-foot">
            Already have an account?{' '}
            <a href="#" onClick={(e) => { e.preventDefault(); onNavigate('login'); }}>Sign in</a>
          </p>
        </form>
      </div>
    </div>
  );
}

function ForgotScreen({ onNavigate }) {
  const [email, setEmail] = useState('');
  const [sent, setSent] = useState(false);
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (loading || !email.trim()) return;
    setLoading(true);
    try {
      // Backend always returns OK to prevent email enumeration.
      await fetch('/api/auth/forgot', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: email.trim() }),
      });
      setSent(true);
    } catch {}
    setLoading(false);
  };

  return (
    <div className="auth-page">
      <img className="auth-landscape" src={`${ASSETS}/landscape-hero.png`} alt="" aria-hidden="true" />
      <div className="auth-card">
        <div className="auth-brand">OYC<span className="reg">®</span></div>

        {!sent ? (
          <>
            <h1 className="auth-headline">
              Forgot password?<br />
              <span className="auth-headline-accent">No problem.</span>
            </h1>
            <p className="auth-subhead">We'll send a reset link to your email.</p>

            <form className="auth-form" onSubmit={handleSubmit}>
              <label className="auth-field">
                <span className="auth-label">Email</span>
                <input className="auth-input" type="email" required value={email} onChange={e => setEmail(e.target.value)} placeholder="you@example.com" autoComplete="email" autoFocus />
              </label>
              <button type="submit" className="auth-submit" disabled={loading}>
                {loading ? 'Sending…' : 'Send reset link'}
              </button>
            </form>
          </>
        ) : (
          <div className="auth-success">
            <div className="auth-success-emoji">📬</div>
            <div className="auth-success-title">Check your inbox</div>
            <div className="auth-success-detail">
              We sent a reset link to <strong>{email}</strong>.
            </div>
          </div>
        )}

        <div className="auth-back-link">
          <button onClick={() => onNavigate('login')}>← Back to sign in</button>
        </div>
      </div>
    </div>
  );
}

// =====================================================================
// PricingScreen — plan browse + upgrade picker.
// Two modes:
//   - Pre-signup (no `user`): wraps with version toggle + sign-in link.
//   - Signed-in (`user` provided): renders inline inside the studio shell;
//     upgrade buttons start checkout, current plan shows a badge.
// =====================================================================
const FREE_PLAN = {
  id: 'free',
  name: 'Free',
  price: 0,
  period: 'forever',
  features: [
    'Try every AI tool',
    '10 trial messages',
    'Community access',
  ],
};

function PricingScreen({ onNavigate, user, embedded }) {
  const cfg = (typeof STUDIO_CONFIG !== 'undefined') ? STUDIO_CONFIG : {};
  const paidPlans = cfg.plans || [];
  const allPlans = [FREE_PLAN, ...paidPlans];
  const currentPlanId = user?.plan || null;

  const handleSelect = (plan) => {
    if (plan.id === currentPlanId) return;            // already on this plan
    if (plan.id === 'free') {
      // Signed-in paid user → portal to manage/cancel. Signed-out → signup.
      if (user && user?.plan_grant?.billing_managed !== false) openBillingPortal();
      else if (user) onNavigate('settings');
      else onNavigate('signup');
      return;
    }
    if (user) startCheckout(plan.id, 'monthly');
    else onNavigate('signup');
  };

  const ctaLabel = (plan) => {
    if (plan.id === currentPlanId) return 'Current plan';
    if (plan.id === 'free') return user ? (user?.plan_grant?.billing_managed === false ? 'Free after grant' : 'Manage billing') : 'Get started';
    if (currentPlanId && currentPlanId !== 'free') return `Switch to ${plan.name}`;
    return `Choose ${plan.name}`;
  };

  return (
    <div className={`pricing-page${embedded ? ' pricing-page--embedded' : ''}`}>
      {embedded ? (
        <header className="hero">
          <h1 className="hero-title">Pick your plan.</h1>
          <p className="hero-sub">The one that fits where you are right now.</p>
        </header>
      ) : (
        <div className="pricing-header">
          <h1 className="pricing-headline">
            Your tools.<br />
            <span className="pricing-headline-accent">Infinite leverage.</span>
          </h1>
          <p className="pricing-subhead">Pick the plan that fits where you are right now.</p>
        </div>
      )}

      <div className="pricing-grid">
        {allPlans.map(plan => {
          const isCurrent = plan.id === currentPlanId;
          const priceDisplay = plan.price === 0 ? '$0' : `$${plan.price.toLocaleString()}`;
          const periodDisplay = plan.id === 'free' ? '' : `/ ${plan.period || 'mo'}`;
          return (
            <article key={plan.id} className={`pricing-card${isCurrent ? ' pricing-card--current' : ''}`}>
              <div className="pricing-card-top">
                <h3 className="pricing-card-name">{plan.name}</h3>
                {isCurrent && <span className="pricing-card-badge">Current plan</span>}
                <ul className="pricing-card-features">
                  {(plan.features || []).map(f => (
                    <li key={f}>
                      <span className="pricing-card-check">✓</span>
                      <span>{f}</span>
                    </li>
                  ))}
                </ul>
              </div>
              <div className="pricing-card-bottom">
                <div className="pricing-card-price">
                  <span className="pricing-card-price-amount">{priceDisplay}</span>
                  {periodDisplay && <span className="pricing-card-price-period">{periodDisplay}</span>}
                </div>
                {plan.priceNote && <p className="pricing-card-pricenote">{plan.priceNote}</p>}
                <button
                  className="pricing-card-cta"
                  disabled={isCurrent}
                  onClick={() => handleSelect(plan)}
                >
                  {ctaLabel(plan)}
                </button>
              </div>
            </article>
          );
        })}
      </div>

      {!embedded && (
        <div className="pricing-foot">
          <p>Cancel anytime.</p>
          <button onClick={() => onNavigate('login')}>Already have an account? Sign in →</button>
        </div>
      )}

    </div>
  );
}

// =====================================================================
// WelcomeModal — first-session greeting on the dashboard. Dismissed
// permanently via localStorage flag.
// =====================================================================
function WelcomeModal({ onDismiss }) {
  return (
    <div className="welcome-overlay" onClick={onDismiss}>
      <div className="welcome-modal" onClick={e => e.stopPropagation()}>
        <div className="welcome-emoji">👋</div>
        <h2 className="welcome-title">Welcome to {APP_NAME}</h2>
        <p className="welcome-message">Here are your AI tools. Click any tool on My Tools to get started.</p>
        <button className="welcome-cta" onClick={onDismiss}>Let's go</button>
      </div>
    </div>
  );
}

function ResetScreen({ token, onNavigate, addToast }) {
  const [password, setPassword] = useState('');
  const [showPwd, setShowPwd] = useState(false);
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (loading) return;
    if (!password || password.length < 8) {
      setError('Password must be at least 8 characters.');
      return;
    }
    setError('');
    setLoading(true);
    try {
      const res = await fetch('/api/auth/reset', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token, password }),
      });
      const data = await res.json().catch(() => ({}));
      if (res.ok) {
        addToast?.({ kind: 'success', message: 'Password reset! Sign in with your new password.' });
        window.history.replaceState({}, '', window.location.pathname);
        onNavigate('login');
      } else {
        setError(data.error || 'Reset failed. The link may be expired.');
      }
    } catch {
      setError('Could not connect. Try again.');
    }
    setLoading(false);
  };

  return (
    <div className="auth-page">
      <img className="auth-landscape" src={`${ASSETS}/landscape-hero.png`} alt="" aria-hidden="true" />
      <div className="auth-card">
        <div className="auth-brand">OYC<span className="reg">®</span></div>

        <h1 className="auth-headline">
          New password.<br />
          <span className="auth-headline-accent">Almost there.</span>
        </h1>
        <p className="auth-subhead">Enter your new password below.</p>

        <form className="auth-form" onSubmit={handleSubmit}>
          <label className="auth-field">
            <span className="auth-label">New password</span>
            <div className="auth-password-wrap">
              <input className="auth-input" type={showPwd ? 'text' : 'password'} required value={password} onChange={e => setPassword(e.target.value)} placeholder="Min. 8 characters" autoComplete="new-password" autoFocus />
              <button type="button" className="auth-password-toggle" onClick={() => setShowPwd(s => !s)} aria-label={showPwd ? 'Hide password' : 'Show password'}>
                <EyeIcon shown={showPwd} />
              </button>
            </div>
          </label>

          {error && <p className="auth-error">{error}</p>}

          <button type="submit" className="auth-submit" disabled={loading}>
            {loading ? 'Resetting…' : 'Reset password'}
          </button>
        </form>

        <div className="auth-back-link">
          <button onClick={() => onNavigate('login')}>← Back to sign in</button>
        </div>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------
// URL state helpers — encode every page the user can navigate to into
// the URL hash so refresh, back/forward, and link-sharing all behave.
//
// Authenticated:
//   /                                → Dashboard
//   /#tools                          → Tools
//   /#training                       → Training (module list)
//   /#training/{lessonId}            → Open a specific lesson
//   /#settings                       → Settings
//   /#admin                          → Admin (default: Users tab)
//   /#admin/{tab}                    → Admin: users|plugins|analytics|errors|financial|system
//   /#pricing                        → Pricing picker
//   /#chat-jed-pov-writer            → Jed, fresh chat
//   /#chat-jed-pov-writer/{convoId}  → Jed, that conversation
//
// Pre-auth (only honored when not signed in):
//   /#login | /#signup | /#forgot | /#reset | /#pricing
// ---------------------------------------------------------------------

const PRE_AUTH_HASHES = new Set(['login', 'signup', 'forgot', 'reset', 'pricing']);

function parseV2Hash(hash) {
  const clean = (hash || '').replace(/^#/, '');
  if (!clean) return { nav: 'dashboard', sub: null, convoId: null };
  const parts = clean.split('/');
  const nav = parts[0];
  if (nav.startsWith('chat-')) {
    return { nav, sub: null, convoId: parts[1] || null };
  }
  return { nav, sub: parts[1] || null, convoId: null };
}

function buildV2Hash(nav, sub, convoId) {
  if (!nav || nav === 'dashboard') return '';
  if (nav.startsWith('chat-')) {
    return convoId ? `${nav}/${convoId}` : nav;
  }
  return sub ? `${nav}/${sub}` : nav;
}

function writeHash(value) {
  const current = window.location.hash.replace(/^#/, '');
  if (value === current) return;
  const url = window.location.pathname + window.location.search + (value ? `#${value}` : '');
  window.history.replaceState(null, '', url);
}

// ---------------------------------------------------------------------
// App shell
// ---------------------------------------------------------------------

function App() {
  // status: 'loading' | 'login' | 'signup' | 'forgot' | 'reset' | 'pricing' | 'ready'
  const [status, setStatus] = useState('loading');
  const [resetToken, setResetToken] = useState(null);
  const [user, setUser] = useState(null);
  const [plugins, setPlugins] = useState([]);
  // Initialise nav + sub-route + active conversation from the URL hash
  // so links / refreshes land on the right screen.
  const [activeNav, setActiveNav] = useState(() => parseV2Hash(window.location.hash).nav);
  const [subRoute, setSubRoute] = useState(() => parseV2Hash(window.location.hash).sub);
  const [toasts, setToasts] = useState([]);
  const [pinnedChats, setPinnedChats] = useState([]);
  const [pendingConvoId, setPendingConvoId] = useState(() => parseV2Hash(window.location.hash).convoId);
  const [historyOpen, setHistoryOpen] = useState(false);
  const [historyConvos, setHistoryConvos] = useState([]);
  const [theme, setTheme] = useState(() => {
    try {
      return localStorage.getItem('v2_theme') || 'light';
    } catch { return 'light'; }
  });

  useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme);
    try { localStorage.setItem('v2_theme', theme); } catch {}
  }, [theme]);

  // Sync state → URL hash whenever the user navigates inside the app.
  useEffect(() => {
    if (status === 'ready') {
      writeHash(buildV2Hash(activeNav, subRoute, pendingConvoId));
      return;
    }
    if (PRE_AUTH_HASHES.has(status)) {
      // Only update the URL when navigating BETWEEN pre-auth screens or
      // from an empty root. If the hash already points at a post-auth
      // deep link (e.g. /#chat-jed/abc123), leave it so the user lands
      // there after sign-in instead of getting bounced to /#login.
      const { nav } = parseV2Hash(window.location.hash);
      if (!nav || nav === 'dashboard' || PRE_AUTH_HASHES.has(nav)) {
        writeHash(status);
      }
    }
  }, [status, activeNav, subRoute, pendingConvoId]);

  // Listen for browser back/forward (and external hash edits).
  useEffect(() => {
    const handler = () => {
      const { nav, sub, convoId } = parseV2Hash(window.location.hash);
      // Pre-auth: if not signed in, the hash drives `status`.
      if (status !== 'ready' && PRE_AUTH_HASHES.has(nav)) {
        setStatus(nav);
        return;
      }
      setActiveNav(prev => prev === nav ? prev : nav);
      setSubRoute(prev => prev === sub ? prev : sub);
      setPendingConvoId(prev => prev === convoId ? prev : convoId);
    };
    window.addEventListener('hashchange', handler);
    return () => window.removeEventListener('hashchange', handler);
  }, [status]);

  const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark');

  const loadPinnedChats = async () => {
    try {
      const res = await fetch('/api/conversations/pinned', { credentials: 'include' });
      if (res.ok) {
        const data = await res.json();
        setPinnedChats(data.conversations || []);
      }
    } catch {}
  };

  const loadAllHistory = async () => {
    try {
      const res = await fetch('/api/conversations', { credentials: 'include' });
      if (res.ok) {
        const data = await res.json();
        setHistoryConvos(data.conversations || []);
      }
    } catch {}
  };

  const openHistoryDrawer = () => {
    loadAllHistory();
    setHistoryOpen(true);
  };
  const closeHistoryDrawer = () => setHistoryOpen(false);

  const openConvoFromHistory = (convo) => {
    setHistoryOpen(false);
    setPendingConvoId(convo.id);
    setActiveNav(`chat-${convo.plugin_id}`);
  };

  const deleteConvoFromHistory = async (convoId) => {
    if (!window.confirm('Delete this conversation?')) return;
    try {
      await fetch(`/api/conversations/${convoId}`, { method: 'DELETE', credentials: 'include' });
      setHistoryConvos(prev => prev.filter(c => c.id !== convoId));
      loadPinnedChats();
    } catch {}
  };

  const renameConvoFromHistory = async (convoId, title) => {
    try {
      await fetch(`/api/conversations/${convoId}`, {
        method: 'PATCH',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title }),
      });
      setHistoryConvos(prev => prev.map(c => c.id === convoId ? { ...c, title } : c));
      loadPinnedChats();
    } catch {}
  };

  // Refresh pinned chats once we're authenticated.
  useEffect(() => {
    if (status === 'ready') loadPinnedChats();
  }, [status]);

  useEffect(() => { document.title = APP_NAME; }, []);

  const addToast = (toast) => {
    const id = Date.now() + Math.random();
    setToasts(prev => [...prev, { ...toast, id }]);
  };
  const dismissToast = (id) => setToasts(prev => prev.filter(t => t.id !== id));

  const loadAppData = async () => {
    try {
      const [userRes, pluginsRes] = await Promise.all([
        fetch('/api/auth/me', { credentials: 'include' }),
        fetch('/api/plugins', { credentials: 'include' }),
      ]);
      if (!userRes.ok) {
        // Honor a pre-auth hash on first load so /#signup, /#forgot, etc.
        // land the user on the right screen instead of always login.
        const { nav } = parseV2Hash(window.location.hash);
        setStatus(PRE_AUTH_HASHES.has(nav) ? nav : 'login');
        return;
      }
      const userData = await userRes.json();
      const pluginsData = await pluginsRes.json();
      setUser(userData.user);
      setPlugins(pluginsData.plugins || []);
      const notifications = userData.notifications || [];
      if (notifications.length) {
        notifications.forEach(n => addToast({ kind: n.kind === 'plan_grant_expired' ? 'info' : (n.kind || 'info'), message: n.message }));
        fetch('/api/notifications/read', {
          method: 'POST',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ ids: notifications.map(n => n.id) }),
        }).catch(() => {});
      }
      setStatus('ready');
    } catch {
      setStatus('login');
    }
  };

  // Refresh just the plugins list, without re-validating the session.
  // Used after admin actions (save / reset / create plugin) where we already
  // know the user is authenticated — re-querying /api/auth/me and bailing on
  // a transient 401 would silently drop the post-save table refresh.
  const loadPlugins = async () => {
    try {
      const res = await fetch('/api/plugins', { credentials: 'include' });
      if (res.ok) {
        const data = await res.json();
        setPlugins(data.plugins || []);
      }
    } catch {}
  };

  // Initial mount — handle URL params (reset token, verified flag) then load data.
  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    if (params.get('reset')) {
      setResetToken(params.get('reset'));
      setStatus('reset');
      return;
    }
    if (params.get('verified')) {
      addToast({ kind: 'success', message: 'Email verified!' });
      window.history.replaceState({}, '', window.location.pathname);
    }
    loadAppData();
  }, []);

  const openPlugin = (pluginId) => {
    setPendingConvoId(null); // start a fresh conversation
    setActiveNav(`chat-${pluginId}`);
  };

  const openPinnedChat = (convo) => {
    setPendingConvoId(convo.id);
    setActiveNav(`chat-${convo.plugin_id}`);
  };

  const toggleFavorite = async (pluginId) => {
    const current = user?.favorite_plugins || [];
    const isFav = current.includes(pluginId);
    const next = isFav ? current.filter(id => id !== pluginId) : [...current, pluginId];
    setUser(prev => ({ ...prev, favorite_plugins: next.length ? next : null }));
    try {
      await fetch('/api/favorites', {
        method: 'PUT',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ favorites: next }),
      });
    } catch {
      setUser(prev => ({ ...prev, favorite_plugins: current.length ? current : null }));
    }
  };

  const handleLogout = async () => {
    try {
      await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
    } catch {}
    window.location.href = '/';
  };

  // Stays inside v2 — every section renders content within the AppShell.
  // Pages that aren't built out yet show a Placeholder with an optional v1 fallback link.
  const handleNav = (target) => {
    // Clear any pending conversation id when leaving a chat for somewhere else.
    if (!target || !target.startsWith('chat-')) setPendingConvoId(null);
    // Sub-routes belong to a single section; clear when changing sections.
    if (target !== activeNav) setSubRoute(null);
    setActiveNav(target);
  };

  // Auth screens — render before App chrome
  if (status === 'loading') {
    return <div className="loading">Loading…</div>;
  }
  if (status === 'login') {
    return (
      <>
        <LoginScreen onAuth={loadAppData} onNavigate={setStatus} />
        <ToastStack toasts={toasts} onDismiss={dismissToast} />
      </>
    );
  }
  if (status === 'signup') {
    return (
      <>
        <SignupScreen onAuth={loadAppData} onNavigate={setStatus} />
        <ToastStack toasts={toasts} onDismiss={dismissToast} />
      </>
    );
  }
  if (status === 'forgot') {
    return (
      <>
        <ForgotScreen onNavigate={setStatus} />
        <ToastStack toasts={toasts} onDismiss={dismissToast} />
      </>
    );
  }
  if (status === 'reset') {
    return (
      <>
        <ResetScreen token={resetToken} onNavigate={setStatus} addToast={addToast} />
        <ToastStack toasts={toasts} onDismiss={dismissToast} />
      </>
    );
  }
  if (status === 'pricing') {
    return (
      <>
        <PricingScreen onNavigate={setStatus} />
        <ToastStack toasts={toasts} onDismiss={dismissToast} />
      </>
    );
  }

  let content;
  if (activeNav === 'dashboard') {
    content = (
      <DashboardContent user={user} plugins={plugins} onNav={setActiveNav} />
    );
  } else if (activeNav.startsWith('chat-')) {
    const pluginId = activeNav.replace('chat-', '');
    const plugin = plugins.find(p => p.id === pluginId);
    content = plugin ? (
      <ChatScreen
        plugin={plugin}
        user={user}
        initialConversationId={pendingConvoId}
        onConversationChange={(newConvoId) => {
          // Capture the new conversation id so the URL reflects it.
          if (newConvoId) setPendingConvoId(newConvoId);
          loadPinnedChats();
        }}
        onPinnedChange={loadPinnedChats}
        onOpenHistory={openHistoryDrawer}
      />
    ) : (
      <PlaceholderContent
        title="Tool not found"
        message="That tool isn't available right now."
        ctaLabel="← Back to tools"
        ctaHref="#tools"
      />
    );
  } else if (activeNav === 'admin' && user?.role === 'admin') {
    content = (
      <AdminContent
        user={user}
        plugins={plugins}
        onPluginsChange={loadPlugins}
        tab={subRoute}
        onTabChange={(t) => setSubRoute(t)}
      />
    );
  } else if (activeNav === 'tools') {
    content = (
      <ToolsContent
        plugins={plugins}
        user={user}
        onOpenPlugin={openPlugin}
        onToggleFavorite={toggleFavorite}
      />
    );
  } else if (activeNav === 'training') {
    content = (
      <TrainingContent
        user={user}
        activeLessonId={subRoute}
        onLessonChange={(id) => setSubRoute(id || null)}
      />
    );
  } else if (activeNav === 'pricing') {
    content = <PricingScreen user={user} embedded onNavigate={setActiveNav} />;
  } else if (activeNav === 'settings') {
    content = <SettingsContent user={user} onUpdate={loadAppData} onNav={setActiveNav} />;
  } else {
    content = <DashboardContent user={user} plugins={plugins} onNav={setActiveNav} />;
  }

  // First-session welcome modal — once per browser via localStorage flag.
  const showWelcome = typeof window !== 'undefined' && !localStorage.getItem('v2_welcomed');
  const dismissWelcome = () => {
    try { localStorage.setItem('v2_welcomed', '1'); } catch {}
    // Force re-render by adding then dismissing a no-op toast (simplest)
    setToasts(prev => [...prev]);
  };

  return (
    <>
      <AppShell
        user={user}
        activeNav={activeNav}
        onNav={handleNav}
        onLogout={handleLogout}
        pinnedChats={pinnedChats}
        onOpenPinnedChat={openPinnedChat}
        onRenamePinnedChat={renameConvoFromHistory}
        plugins={plugins}
        onOpenPlugin={openPlugin}
        theme={theme}
        onToggleTheme={toggleTheme}
        historyOpen={historyOpen}
        historyConvos={historyConvos}
        onCloseHistory={closeHistoryDrawer}
        onOpenHistoryConvo={openConvoFromHistory}
        onDeleteHistoryConvo={deleteConvoFromHistory}
        onRenameHistoryConvo={renameConvoFromHistory}
      >
        {content}
      </AppShell>
      {showWelcome && <WelcomeModal onDismiss={dismissWelcome} />}
      <ToastStack toasts={toasts} onDismiss={dismissToast} />
    </>
  );
}

ReactDOM.createRoot(document.getElementById('app')).render(React.createElement(App));
