// Kayser v2 — Threads
// Auto-grouped conversation threads. Owners don't create them; they exist because the entity exists.
// Each thread is a "scope" — when you pick one, the chat filters to just that conversation
// and the composer becomes context-scoped. There's always a "Now" thread (everything, unscoped).
//
// In live mode (window.KX_TOKEN present), the rail and the mobile sheet
// also render user-created folders. Each non-"now" thread row has a
// hover-revealed 3-dots menu (Rename · Move to ▸ · Delete) and the rail
// header gets a "+ Folder" affordance. Folder list lives in app.jsx
// state and is fetched from /api/v1/folders alongside /api/v1/threads.

// ============ MOCK THREADS ============
// In production these are entity-derived in the API. Here we hardcode a realistic mix.
// `kind` controls the small icon + sort priority. `slug` is the thread id.
// `summary` is a one-line description Kayser keeps fresh ("→ awaiting approval → 4d quiet").
// `tags` filter Now-stream messages into this thread (basic substring match for the demo).

const KXV2_THREADS = [
  { slug: 'now',           kind: 'now',       name: 'Now',                         summary: 'Everything Kayser is watching · default home',                pinned: true, unread: 4 },

  // ENTITY: customers
  { slug: 'lina-garcia',   kind: 'customer',  name: 'Lina Garcia',                 summary: 'Reminder sent · awaiting reply · 14m',                        pinned: true, unread: 0, tags: ['Garcia', 'Lina', 'Maria'] },
  { slug: 'patel-family',  kind: 'customer',  name: 'Patel family',                summary: 'Exterior repaint · moved to Thursday · weather hold',         pinned: true, unread: 1, tags: ['Patel'] },
  { slug: 'henderson-lead',kind: 'lead',      name: 'Henderson · lead',            summary: 'Pier & beam quote · cold 6 days · auto-followup queued',      pinned: false, unread: 1, tags: ['Henderson'] },
  { slug: 'reyes-bakery',  kind: 'customer',  name: 'Reyes Bakery',                summary: 'Site visit booked Tue · new lead Apr 25',                     pinned: false, unread: 0, tags: ['Reyes'] },
  { slug: 'chen-kitchen',  kind: 'customer',  name: 'Chen kitchen',                summary: 'Finishing trim · job wraps today',                            pinned: false, unread: 0, tags: ['Chen'] },

  // ENTITY: jobs / projects
  { slug: 'invoice-1031',  kind: 'invoice',   name: 'Invoice #1031',               summary: '$4,800 · 7d overdue · nudge sent',                            pinned: false, unread: 0, tags: ['#1031', '1031'] },
  { slug: 'patel-exterior',kind: 'job',       name: 'Patel exterior',              summary: 'Day 1 of 3 · Thu 9 AM · 70% rain Wed',                        pinned: false, unread: 1, tags: ['Patel exterior', 'repaint'] },
  { slug: 'williams-porch',kind: 'job',       name: 'Williams porch',              summary: 'Estimate today 1 PM · 442 Cedar',                             pinned: false, unread: 0, tags: ['Williams'] },

  // ENTITY: vendors / receipts
  { slug: 'sherwin-receipts', kind: 'vendor', name: 'Sherwin Williams receipts',   summary: '3 logged this month · auto-matched to jobs',                  pinned: false, unread: 0, tags: ['Sherwin', 'paint'] },

  // ENTITY: cross-cutting (Kayser proposes these — user confirmed)
  { slug: 'cashflow',      kind: 'topic',     name: 'Cashflow (this month)',       summary: '$11,200 outstanding · 3 invoices · auto-nudge plan drafted',  pinned: false, unread: 2, tags: ['overdue', 'invoice', 'cashflow', '#1029', '#1028'] },
];

// Sort order: pinned first, then unread > 0 (most unread first), then alpha
function kxv2SortThreads(threads) {
  return [...threads].sort((a, b) => {
    if (a.slug === 'now') return -1; if (b.slug === 'now') return 1;
    if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
    const aU = a.unread || 0, bU = b.unread || 0;
    if (aU !== bU) return bU - aU;
    return a.name.localeCompare(b.name);
  });
}

// Filter messages from the master stream into a given thread.
//
// Live messages (from /api/v1/...) all carry a `thread` field on every
// envelope. When ANY message has `thread` set we treat that as
// authoritative — return only the rows that match (or are unattributed
// system bubbles like typing dots). This is the only path that
// matters for Brothers.
//
// The fall-through (tag-substring matching against KXV2_THREADS) only
// fires in the standalone prototype demo where messages come from
// data.jsx mocks without `thread` fields.
function kxv2FilterMessages(messages, threadSlug) {
  const liveTagged = messages.some(m => m.thread);
  if (liveTagged) {
    return messages.filter(m => !m.thread || m.thread === threadSlug);
  }
  // Prototype demo path
  if (!threadSlug || threadSlug === 'now') return messages;
  const t = KXV2_THREADS.find(x => x.slug === threadSlug);
  if (!t || !t.tags) return [];
  const tags = t.tags.map(x => x.toLowerCase());
  return messages.filter(m => {
    const haystack = [
      m.text || '',
      m.card?.title || '',
      m.card?.subtitle || '',
      m.card?.body || '',
      m.card?.meta || '',
    ].join(' ').toLowerCase();
    return tags.some(tag => haystack.includes(tag));
  });
}

// ============ THREAD ICON ============
function ThreadIcon({ kind, size = 28 }) {
  const map = {
    now:       { bg: 'rgba(124,58,237,.25)', fg: '#c4b5fd', glyph: '◉' },
    customer:  { bg: 'rgba(96,165,250,.20)', fg: '#bfdbfe', glyph: 'C' },
    lead:      { bg: 'rgba(251,191,36,.20)', fg: '#fde68a', glyph: 'L' },
    job:       { bg: 'rgba(52,211,153,.20)', fg: '#a7f3d0', glyph: 'J' },
    invoice:   { bg: 'rgba(251,113,133,.20)', fg: '#fda4af', glyph: '$' },
    vendor:    { bg: 'rgba(255,255,255,.10)', fg: 'rgba(255,255,255,.7)', glyph: 'V' },
    topic:     { bg: 'rgba(124,58,237,.20)', fg: '#c4b5fd', glyph: '◆' },
  }[kind] || { bg: 'rgba(255,255,255,.08)', fg: 'rgba(255,255,255,.5)', glyph: '·' };
  return (
    <div style={{
      width: size, height: size, borderRadius: size / 2,
      background: map.bg, color: map.fg, flexShrink: 0,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      fontFamily: KX2.fontMono, fontSize: size <= 24 ? 11 : 12, fontWeight: 700,
    }}>{map.glyph}</div>
  );
}

// ============ THREAD ROW MENU ============
// Small popover anchored to the 3-dots. Stays open until the user picks
// an action or clicks outside. Two views: the root list (Rename / Move /
// Delete) and the move-to-folder picker — both rendered inline (no true
// submenu) so we don't have to deal with portal positioning math.
function ThreadRowMenu({
  thread, folders, onClose, onRename, onMove, onDelete,
}) {
  const [view, setView] = React.useState('root'); // 'root' | 'move'
  const ref = React.useRef(null);
  React.useEffect(() => {
    const onDown = (e) => {
      if (ref.current && !ref.current.contains(e.target)) onClose?.();
    };
    const onKey = (e) => { if (e.key === 'Escape') onClose?.(); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDown);
      document.removeEventListener('keydown', onKey);
    };
  }, [onClose]);

  const itemStyle = {
    display: 'flex', alignItems: 'center', gap: 8,
    padding: '8px 12px', borderRadius: 8,
    background: 'transparent', border: 'none', cursor: 'pointer',
    fontFamily: KX2.fontUI, fontSize: 13, color: KX2.ink,
    width: '100%', textAlign: 'left',
  };

  return (
    <div ref={ref} onClick={(e) => e.stopPropagation()} style={{
      position: 'absolute', right: 8, top: 36, zIndex: 50,
      ...kx2Glass({ borderRadius: 12 }),
      padding: 4, minWidth: 180, maxWidth: 240,
      boxShadow: '0 10px 30px rgba(0,0,0,.4)',
    }}>
      {view === 'root' ? (
        <>
          <button style={itemStyle}
            onClick={() => { onRename?.(thread); onClose?.(); }}
            onMouseEnter={(e) => { e.currentTarget.style.background = KX2.chipBg; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
            <span style={{ width: 16, color: KX2.ink3 }}>✎</span> Rename
          </button>
          <button style={itemStyle}
            onClick={() => setView('move')}
            onMouseEnter={(e) => { e.currentTarget.style.background = KX2.chipBg; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
            <span style={{ width: 16, color: KX2.ink3 }}>▸</span>
            <span style={{ flex: 1 }}>Move to folder</span>
            <span style={{ color: KX2.ink3, fontSize: 11 }}>›</span>
          </button>
          <div style={{ height: 1, background: KX2.glassLine, margin: '4px 6px' }} />
          <button style={{ ...itemStyle, color: '#fca5a5' }}
            onClick={() => { onDelete?.(thread); onClose?.(); }}
            onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(248,113,113,.12)'; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
            <span style={{ width: 16 }}>🗑</span> Delete
          </button>
        </>
      ) : (
        <>
          <button style={{ ...itemStyle, color: KX2.ink3, fontSize: 11.5, fontFamily: KX2.fontMono }}
            onClick={() => setView('root')}
            onMouseEnter={(e) => { e.currentTarget.style.background = KX2.chipBg; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
            <span style={{ width: 16 }}>‹</span> BACK
          </button>
          <div style={{ height: 1, background: KX2.glassLine, margin: '4px 6px' }} />
          {(folders || []).map((f) => (
            <button key={f.id} style={itemStyle}
              onClick={() => { onMove?.(thread, f.id); onClose?.(); }}
              onMouseEnter={(e) => { e.currentTarget.style.background = KX2.chipBg; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
              <span style={{ width: 16, color: KX2.ink3 }}>▦</span>
              <span style={{
                flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
              }}>{f.name}</span>
              {String(thread.folder_id) === String(f.id) && (
                <span style={{ color: KX2.brand, fontSize: 11 }}>✓</span>
              )}
            </button>
          ))}
          {thread.folder_id && (
            <>
              <div style={{ height: 1, background: KX2.glassLine, margin: '4px 6px' }} />
              <button style={itemStyle}
                onClick={() => { onMove?.(thread, null); onClose?.(); }}
                onMouseEnter={(e) => { e.currentTarget.style.background = KX2.chipBg; }}
                onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
                <span style={{ width: 16, color: KX2.ink3 }}>↺</span> Remove from folder
              </button>
            </>
          )}
          {(!folders || !folders.length) && (
            <div style={{
              padding: '8px 12px', fontFamily: KX2.fontUI, fontSize: 12, color: KX2.ink3,
            }}>
              No folders yet. Create one from the sidebar.
            </div>
          )}
        </>
      )}
    </div>
  );
}

// ============ THREAD ROW ============
// Renders one thread. If `onMenu` is provided (live mode only), shows a
// hover-revealed 3-dots button on the right. The menu itself is owned
// by the rail/sheet — the row just emits the open intent.
function ThreadRow({
  thread, active, onClick, dense,
  menuOpen, onMenuToggle,
  folders, onRename, onMove, onDelete,
}) {
  const [hover, setHover] = React.useState(false);
  // Hide the 3-dots menu for special pseudo-threads: the always-on
  // "now" thread (no row exists, can't rename/move/delete) and the
  // local "__draft__" sentinel (also no row, vanishes on send).
  const isSpecial = thread.slug === 'now' || thread.slug === '__draft__';
  const showMenuButton = !!onMenuToggle && !isSpecial;
  const showButton = showMenuButton && (hover || menuOpen);
  // We render the row as a div (not <button>) so the menu can be a
  // sibling without nesting interactive elements. Click on the row body
  // selects; click on the 3-dots stops propagation and opens the menu.
  return (
    <div style={{ position: 'relative' }}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
    >
      <div
        role="button"
        tabIndex={0}
        onClick={onClick}
        onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick?.(); } }}
        style={{
          display: 'flex', alignItems: 'center', gap: 10, width: '100%',
          padding: dense ? '8px 10px' : '10px 12px',
          borderRadius: 10, cursor: 'pointer', textAlign: 'left',
          border: 'none',
          background: active ? KX2.chipBgActive : (hover ? KX2.chipBg : 'transparent'),
          fontFamily: KX2.fontUI, color: KX2.ink, marginBottom: 2,
        }}
      >
        <ThreadIcon kind={thread.kind} size={dense ? 24 : 28} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
            <span style={{
              fontSize: dense ? 13 : 14, fontWeight: 600, color: KX2.ink,
              whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1, minWidth: 0,
            }}>{thread.name}</span>
            {thread.pinned && (
              <span style={{ fontFamily: KX2.fontMono, fontSize: 9, color: KX2.ink3, letterSpacing: 0.3 }}>PIN</span>
            )}
          </div>
          <div style={{
            fontSize: 11.5, color: KX2.ink3, marginTop: 1, lineHeight: 1.35,
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          }}>{thread.summary}</div>
        </div>
        {thread.unread > 0 && !showButton && (
          <div style={{
            minWidth: 18, height: 18, borderRadius: 9, padding: '0 6px',
            background: KX2.brand, color: '#fff',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontFamily: KX2.fontMono, fontSize: 10, fontWeight: 700, flexShrink: 0,
          }}>{thread.unread}</div>
        )}
        {showButton && (
          <button
            aria-label="Conversation options"
            onClick={(e) => { e.stopPropagation(); onMenuToggle?.(thread); }}
            style={{
              width: 24, height: 24, borderRadius: 6, border: 'none',
              background: menuOpen ? KX2.chipBgActive : 'transparent',
              color: KX2.ink2, cursor: 'pointer', flexShrink: 0,
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontSize: 16, lineHeight: 1, padding: 0,
            }}
            onMouseEnter={(e) => { e.currentTarget.style.background = KX2.chipBgActive; }}
            onMouseLeave={(e) => {
              e.currentTarget.style.background = menuOpen ? KX2.chipBgActive : 'transparent';
            }}>⋯</button>
        )}
      </div>
      {menuOpen && (
        <ThreadRowMenu
          thread={thread}
          folders={folders}
          onClose={() => onMenuToggle?.(null)}
          onRename={onRename}
          onMove={onMove}
          onDelete={onDelete}
        />
      )}
    </div>
  );
}

// ============ FOLDER SECTION ============
// A collapsible group inside the rail. Header shows the chevron, the
// folder name (inline-editable on rename), the count, and a tiny menu
// button for rename/delete.
function FolderSection({
  folder, threads, activeSlug, dense, defaultOpen = true,
  menuThreadSlug, onMenuToggle,
  folders, onRenameThread, onMoveThread, onDeleteThread,
  onRenameFolder, onDeleteFolder,
  onPickThread,
}) {
  const [open, setOpen] = React.useState(defaultOpen);
  const [folderMenu, setFolderMenu] = React.useState(false);
  const [editing, setEditing] = React.useState(false);
  const [draft, setDraft] = React.useState(folder.name);
  React.useEffect(() => { setDraft(folder.name); }, [folder.name]);
  const folderMenuRef = React.useRef(null);
  React.useEffect(() => {
    if (!folderMenu) return;
    const onDown = (e) => {
      if (folderMenuRef.current && !folderMenuRef.current.contains(e.target)) {
        setFolderMenu(false);
      }
    };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [folderMenu]);

  const commitRename = () => {
    const name = draft.trim();
    setEditing(false);
    if (name && name !== folder.name) onRenameFolder?.(folder, name);
    else setDraft(folder.name);
  };

  return (
    <div style={{ marginTop: 4 }}>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 6,
        padding: '6px 8px 4px', position: 'relative',
      }}>
        <button onClick={() => setOpen((o) => !o)}
          aria-label={open ? 'Collapse folder' : 'Expand folder'}
          style={{
            width: 16, height: 16, border: 'none', background: 'transparent',
            color: KX2.ink3, cursor: 'pointer', padding: 0,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
            transition: 'transform .12s ease',
          }}>
          <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round"><polyline points="9 18 15 12 9 6"/></svg>
        </button>
        {editing ? (
          <input
            autoFocus value={draft}
            onChange={(e) => setDraft(e.target.value)}
            onBlur={commitRename}
            onKeyDown={(e) => {
              if (e.key === 'Enter') commitRename();
              if (e.key === 'Escape') { setEditing(false); setDraft(folder.name); }
            }}
            style={{
              flex: 1, background: KX2.chipBg, border: `1px solid ${KX2.glassLine}`,
              outline: 'none', borderRadius: 6, padding: '2px 6px',
              fontFamily: KX2.fontMono, fontSize: 10, letterSpacing: 0.6,
              color: KX2.ink, textTransform: 'uppercase',
            }}
          />
        ) : (
          <button
            onClick={() => setOpen((o) => !o)}
            onDoubleClick={() => setEditing(true)}
            style={{
              flex: 1, background: 'transparent', border: 'none',
              fontFamily: KX2.fontMono, fontSize: 10, color: KX2.ink3,
              letterSpacing: 0.6, textTransform: 'uppercase',
              textAlign: 'left', padding: 0, cursor: 'pointer',
              whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
            }}>
            {folder.name}
            <span style={{ marginLeft: 6, opacity: 0.6 }}>{threads.length}</span>
          </button>
        )}
        <button
          aria-label="Folder options"
          onClick={() => setFolderMenu((v) => !v)}
          style={{
            width: 22, height: 22, borderRadius: 6, border: 'none',
            background: folderMenu ? KX2.chipBgActive : 'transparent',
            color: KX2.ink3, cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontSize: 14, lineHeight: 1, padding: 0,
          }}>⋯</button>
        {folderMenu && (
          <div ref={folderMenuRef} style={{
            position: 'absolute', right: 4, top: 28, zIndex: 50,
            ...kx2Glass({ borderRadius: 12 }),
            padding: 4, minWidth: 160,
            boxShadow: '0 10px 30px rgba(0,0,0,.4)',
          }}>
            <button onClick={() => { setEditing(true); setFolderMenu(false); }}
              style={{
                display: 'flex', alignItems: 'center', gap: 8, width: '100%',
                padding: '8px 12px', borderRadius: 8, border: 'none',
                background: 'transparent', cursor: 'pointer',
                fontFamily: KX2.fontUI, fontSize: 13, color: KX2.ink, textAlign: 'left',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = KX2.chipBg; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
              <span style={{ width: 16, color: KX2.ink3 }}>✎</span> Rename
            </button>
            <div style={{ height: 1, background: KX2.glassLine, margin: '4px 6px' }} />
            <button onClick={() => { onDeleteFolder?.(folder); setFolderMenu(false); }}
              style={{
                display: 'flex', alignItems: 'center', gap: 8, width: '100%',
                padding: '8px 12px', borderRadius: 8, border: 'none',
                background: 'transparent', cursor: 'pointer',
                fontFamily: KX2.fontUI, fontSize: 13, color: '#fca5a5', textAlign: 'left',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(248,113,113,.12)'; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
              <span style={{ width: 16 }}>🗑</span> Delete folder
            </button>
          </div>
        )}
      </div>
      {open && (
        <div style={{ paddingLeft: 12 }}>
          {threads.map((t) => (
            <ThreadRow key={t.slug} thread={t} dense={dense}
              active={t.slug === activeSlug}
              onClick={() => onPickThread?.(t.slug)}
              menuOpen={menuThreadSlug === t.slug}
              onMenuToggle={onMenuToggle}
              folders={folders}
              onRename={onRenameThread}
              onMove={onMoveThread}
              onDelete={onDeleteThread}
            />
          ))}
          {threads.length === 0 && (
            <div style={{
              padding: '4px 8px 8px', fontFamily: KX2.fontUI,
              fontSize: 11.5, color: KX2.ink3, fontStyle: 'italic',
            }}>Empty folder</div>
          )}
        </div>
      )}
    </div>
  );
}

// ============ THREADS SHEET (mobile / tablet) ============
function KxV2ThreadsSheet({
  open, onClose, activeSlug, onPick, threads, onNew,
  folders, onNewFolder, onRenameFolder, onDeleteFolder,
  onRenameThread, onMoveThread, onDeleteThread,
}) {
  const [q, setQ] = React.useState('');
  const [menuSlug, setMenuSlug] = React.useState(null);
  React.useEffect(() => { if (open) { setQ(''); setMenuSlug(null); } }, [open]);

  // Real threads (from window.__kxThreads / kxApi('/threads')) override
  // the mock list when present. A signed-in operator (KX_TOKEN set) is
  // ALWAYS in live mode — even if `threads` is briefly empty during the
  // first fetch — so they never see fake customer names like "Patel
  // family" / "Lina Garcia" rendered as if they were real. The mock list
  // is reserved for the standalone/file:// prototype with no token.
  const liveMode = !!window.KX_TOKEN || !!(threads && threads.length);
  const newDisabled = activeSlug === '__draft__';
  const source = liveMode ? (threads || []) : KXV2_THREADS;
  const filtered = source.filter(t =>
    !q || t.name.toLowerCase().includes(q.toLowerCase()) ||
    (t.summary || '').toLowerCase().includes(q.toLowerCase())
  );
  const sorted = liveMode ? filtered : kxv2SortThreads(filtered);

  const groups = [
    { id: 'now',     label: '',          match: t => t.slug === 'now' },
    { id: 'pinned',  label: 'Pinned',    match: t => t.slug !== 'now' && t.pinned },
    { id: 'recent',  label: 'Recent',    match: t => t.slug !== 'now' && !t.pinned },
  ];

  const onMenuToggle = (t) => setMenuSlug(t ? (menuSlug === t.slug ? null : t.slug) : null);
  const onPickThread = (slug) => { onPick?.(slug); onClose?.(); };

  // Bucket live-mode threads by folder for the sheet rendering.
  const liveFolders = liveMode ? (folders || []) : [];
  const liveFolderIds = new Set(liveFolders.map((f) => String(f.id)));
  const nowThread = liveMode ? sorted.find((t) => t.slug === 'now') : null;
  const nonNow = liveMode ? sorted.filter((t) => t.slug !== 'now') : [];
  const rootThreads = nonNow.filter((t) => !t.folder_id || !liveFolderIds.has(String(t.folder_id)));
  const folderBuckets = liveFolders.map((f) => ({
    folder: f,
    threads: nonNow.filter((t) => String(t.folder_id) === String(f.id)),
  }));

  return (
    <KxV2Sheet open={open} onClose={onClose} title="Conversations">
      <div style={{ padding: '10px 12px', display: 'flex', gap: 8, alignItems: 'center' }}>
        <div style={{
          ...kx2GlassThin({ borderRadius: 12 }),
          flex: 1,
          display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px',
        }}>
          <span style={{ fontFamily: KX2.fontMono, color: KX2.ink3, fontSize: 14 }}>⌕</span>
          <input autoFocus value={q} onChange={(e) => setQ(e.target.value)}
            placeholder="Search conversations…"
            style={{
              flex: 1, background: 'transparent', border: 'none', outline: 'none',
              color: KX2.ink, fontSize: 14, fontFamily: KX2.fontUI,
            }} />
        </div>
        {onNew && (
          <button onClick={newDisabled ? undefined : () => { onClose?.(); onNew(); }}
            disabled={newDisabled}
            title={newDisabled ? "You're already in a new conversation — type to start." : 'Start a new conversation'}
            style={{
              padding: '8px 14px', borderRadius: 12, border: 'none',
              background: newDisabled ? KX2.chipBg : KX2.brand,
              color: newDisabled ? KX2.ink3 : '#fff',
              cursor: newDisabled ? 'not-allowed' : 'pointer',
              opacity: newDisabled ? 0.6 : 1,
              fontFamily: KX2.fontUI, fontSize: 13, fontWeight: 600,
              boxShadow: newDisabled ? 'none' : '0 6px 18px rgba(124,58,237,.35)',
            }}>+ New</button>
        )}
      </div>
      {liveMode && onNewFolder && (
        <div style={{ padding: '0 12px 8px' }}>
          <button onClick={onNewFolder} style={{
            width: '100%', padding: '6px 10px', borderRadius: 10,
            border: `1px dashed ${KX2.glassLine}`, background: 'transparent',
            color: KX2.ink2, cursor: 'pointer',
            fontFamily: KX2.fontUI, fontSize: 12.5,
            display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
          }}>+ New folder</button>
        </div>
      )}
      <div style={{ padding: '0 8px 16px' }}>
        {liveMode ? (
          <>
            {nowThread && (
              <ThreadRow key={nowThread.slug} thread={nowThread}
                active={nowThread.slug === activeSlug}
                onClick={() => onPickThread(nowThread.slug)} />
            )}
            {folderBuckets.map(({ folder, threads }) => (
              <FolderSection key={folder.id}
                folder={folder} threads={threads}
                activeSlug={activeSlug}
                menuThreadSlug={menuSlug}
                onMenuToggle={onMenuToggle}
                folders={liveFolders}
                onRenameThread={onRenameThread}
                onMoveThread={onMoveThread}
                onDeleteThread={onDeleteThread}
                onRenameFolder={onRenameFolder}
                onDeleteFolder={onDeleteFolder}
                onPickThread={onPickThread}
              />
            ))}
            {rootThreads.length > 0 && (
              <div style={{ marginTop: folderBuckets.length ? 12 : 4 }}>
                {folderBuckets.length > 0 && (
                  <div style={{
                    fontFamily: KX2.fontMono, fontSize: 10, color: KX2.ink3,
                    letterSpacing: 0.6, textTransform: 'uppercase',
                    padding: '6px 14px 4px',
                  }}>Conversations</div>
                )}
                <div style={{ padding: '0 4px' }}>
                  {rootThreads.map(t => (
                    <ThreadRow key={t.slug} thread={t}
                      active={t.slug === activeSlug}
                      onClick={() => onPickThread(t.slug)}
                      menuOpen={menuSlug === t.slug}
                      onMenuToggle={onMenuToggle}
                      folders={liveFolders}
                      onRename={onRenameThread}
                      onMove={onMoveThread}
                      onDelete={onDeleteThread}
                    />
                  ))}
                </div>
              </div>
            )}
          </>
        ) : (
          groups.map(g => {
            const items = sorted.filter(g.match);
            if (!items.length) return null;
            return (
              <div key={g.id} style={{ marginTop: g.id === 'now' ? 4 : 12 }}>
                {g.label && (
                  <div style={{
                    fontFamily: KX2.fontMono, fontSize: 10, color: KX2.ink3,
                    letterSpacing: 0.6, textTransform: 'uppercase',
                    padding: '6px 14px 4px',
                  }}>{g.label}</div>
                )}
                <div style={{ padding: '0 4px' }}>
                  {items.map(t => (
                    <ThreadRow key={t.slug} thread={t}
                      active={t.slug === activeSlug}
                      onClick={() => { onPick?.(t.slug); onClose?.(); }}
                    />
                  ))}
                </div>
              </div>
            );
          })
        )}
      </div>
    </KxV2Sheet>
  );
}

// ============ THREADS RAIL (desktop left rail) ============
function KxV2ThreadsRail({
  activeSlug, onPick, onCollapse, threads, onNew,
  folders, onNewFolder, onRenameFolder, onDeleteFolder,
  onRenameThread, onMoveThread, onDeleteThread,
}) {
  const [q, setQ] = React.useState('');
  const [menuSlug, setMenuSlug] = React.useState(null);
  const [searchHits, setSearchHits] = React.useState([]);
  // A signed-in operator (KX_TOKEN set) is always in live mode — see
  // the matching comment on KxV2ThreadsSheet above. The mock list is
  // reserved for the standalone/file:// prototype with no token.
  const liveMode = !!window.KX_TOKEN || !!(threads && threads.length);
  // "+ New conversation" is a no-op when the user is already sitting on
  // an unsaved draft. Dim the button so they see why nothing happens.
  const newDisabled = activeSlug === '__draft__';
  const source = liveMode ? (threads || []) : KXV2_THREADS;
  const filtered = source.filter(t =>
    !q || t.name.toLowerCase().includes(q.toLowerCase()) ||
    (t.summary || '').toLowerCase().includes(q.toLowerCase())
  );

  // Debounced /search call so the rail finds "that thing I said about
  // Patel" — fires only in live mode, only on queries ≥2 chars. The
  // results are message-content hits (snippets + thread title) that
  // render below the thread-name matches.
  React.useEffect(() => {
    if (!liveMode || !window.kxApi || q.trim().length < 2) {
      setSearchHits([]);
      return;
    }
    const t = setTimeout(async () => {
      try {
        const hits = await window.kxApi('/search?q=' + encodeURIComponent(q.trim()) + '&limit=10');
        setSearchHits(Array.isArray(hits) ? hits : []);
      } catch (_) {
        setSearchHits([]);
      }
    }, 250);
    return () => clearTimeout(t);
  }, [q, liveMode]);
  const searching = q.trim().length >= 2 && liveMode;
  // Live mode: server already orders by GREATEST(last_message_ts,
  // created_at) DESC, so new threads sit at the top. Re-sorting
  // alphabetically (kxv2SortThreads) would bury them. Mock mode
  // keeps the original demo grouping.
  const sorted = liveMode ? filtered : kxv2SortThreads(filtered);
  const groups = [
    { id: 'now',     label: '',          match: t => t.slug === 'now' },
    { id: 'pinned',  label: 'Pinned',    match: t => t.slug !== 'now' && t.pinned },
    { id: 'recent',  label: 'Active',    match: t => t.slug !== 'now' && !t.pinned },
  ];

  const onMenuToggle = (t) => setMenuSlug(t ? (menuSlug === t.slug ? null : t.slug) : null);

  // Bucket live-mode threads by folder. Archived folders (filtered out
  // server-side anyway) won't appear here; if a thread carries a
  // folder_id we don't know about (e.g. stale local state), we fall
  // back to rendering it at root.
  const liveFolders = liveMode ? (folders || []) : [];
  const liveFolderIds = new Set(liveFolders.map((f) => String(f.id)));
  const nowThread = liveMode ? sorted.find((t) => t.slug === 'now') : null;
  const nonNow = liveMode ? sorted.filter((t) => t.slug !== 'now') : [];
  const rootThreads = nonNow.filter(
    (t) => !t.folder_id || !liveFolderIds.has(String(t.folder_id))
  );
  const folderBuckets = liveFolders.map((f) => ({
    folder: f,
    threads: nonNow.filter((t) => String(t.folder_id) === String(f.id)),
  }));

  return (
    <div style={{
      width: 240, flexShrink: 0, height: '100%',
      borderRight: `1px solid ${KX2.glassLine}`,
      display: 'flex', flexDirection: 'column', overflow: 'hidden',
    }}>
      <div style={{
        padding: '12px 12px 8px', display: 'flex', alignItems: 'center', gap: 6,
      }}>
        <div style={{
          ...kx2GlassThin({ borderRadius: 10 }),
          flex: 1, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 10px',
        }}>
          <span style={{ fontFamily: KX2.fontMono, color: KX2.ink3, fontSize: 12 }}>⌕</span>
          <input value={q} onChange={(e) => setQ(e.target.value)}
            placeholder="Search conversations…"
            style={{
              flex: 1, background: 'transparent', border: 'none', outline: 'none',
              color: KX2.ink, fontSize: 12.5, fontFamily: KX2.fontUI,
            }} />
          {q && (
            <button
              onClick={() => setQ('')}
              aria-label="Clear search"
              title="Clear"
              style={{
                width: 16, height: 16, border: 'none', padding: 0,
                background: 'transparent', color: KX2.ink3, cursor: 'pointer',
                fontSize: 12, lineHeight: 1,
              }}>×</button>
          )}
        </div>
        <button onClick={onCollapse} title="Hide conversations" style={{
          width: 28, height: 28, borderRadius: 14, border: 'none',
          background: 'transparent', color: KX2.ink3, cursor: 'pointer',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><polyline points="15 18 9 12 15 6"/></svg>
        </button>
      </div>
      {(onNew || onNewFolder) && (
        <div style={{ padding: '0 12px 8px', display: 'flex', flexDirection: 'column', gap: 6 }}>
          {onNew && (
            <button onClick={newDisabled ? undefined : onNew}
              disabled={newDisabled}
              title={newDisabled ? "You're already in a new conversation — type to start." : 'Start a new conversation'}
              style={{
                width: '100%', padding: '8px 12px', borderRadius: 10, border: 'none',
                background: newDisabled ? KX2.chipBg : KX2.brand,
                color: newDisabled ? KX2.ink3 : '#fff',
                cursor: newDisabled ? 'not-allowed' : 'pointer',
                fontFamily: KX2.fontUI, fontSize: 12.5, fontWeight: 600,
                display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                boxShadow: newDisabled ? 'none' : '0 6px 18px rgba(124,58,237,.3)',
                opacity: newDisabled ? 0.6 : 1,
                transition: 'background .15s, opacity .15s',
              }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round">
                <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
              </svg>
              New conversation
            </button>
          )}
          {liveMode && onNewFolder && (
            <button onClick={onNewFolder} style={{
              width: '100%', padding: '6px 10px', borderRadius: 10,
              border: `1px dashed ${KX2.glassLine}`, background: 'transparent',
              color: KX2.ink2, cursor: 'pointer',
              fontFamily: KX2.fontUI, fontSize: 12, fontWeight: 500,
              display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
            }}>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round">
                <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
              </svg>
              New folder
            </button>
          )}
        </div>
      )}
      <div style={{ flex: 1, overflowY: 'auto', padding: '0 8px 12px' }}>
        {searching && (
          <div style={{ marginBottom: 8 }}>
            <div style={{
              fontFamily: KX2.fontMono, fontSize: 10, color: KX2.ink3,
              letterSpacing: 0.6, textTransform: 'uppercase',
              padding: '4px 10px 4px',
            }}>
              Matches {searchHits.length ? `(${searchHits.length})` : ''}
            </div>
            {searchHits.length === 0 ? (
              <div style={{
                padding: '6px 12px', fontFamily: KX2.fontUI, fontSize: 11.5,
                color: KX2.ink3, fontStyle: 'italic',
              }}>No message matches.</div>
            ) : (
              searchHits.map((h, i) => (
                <button key={i}
                  onClick={() => onPick?.(h.thread_id)}
                  title={h.snippet}
                  style={{
                    display: 'block', width: '100%', textAlign: 'left',
                    background: 'transparent', border: 'none',
                    padding: '6px 10px', borderRadius: 8, cursor: 'pointer',
                    marginBottom: 2,
                  }}
                  onMouseEnter={(e) => { e.currentTarget.style.background = KX2.chipBg; }}
                  onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
                >
                  <div style={{
                    fontFamily: KX2.fontUI, fontSize: 12, fontWeight: 600,
                    color: KX2.ink, lineHeight: 1.2,
                    whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                  }}>{h.thread_title}</div>
                  <div style={{
                    fontFamily: KX2.fontUI, fontSize: 11, color: KX2.ink3,
                    marginTop: 2, lineHeight: 1.3,
                    display: '-webkit-box', WebkitLineClamp: 2,
                    WebkitBoxOrient: 'vertical', overflow: 'hidden',
                  }}>
                    <span style={{
                      fontFamily: KX2.fontMono, fontSize: 9, color: KX2.ink3,
                      marginRight: 4, opacity: 0.7,
                    }}>{h.role === 'user' ? 'YOU' : 'AGENT'}</span>
                    {h.snippet}
                  </div>
                </button>
              ))
            )}
            <div style={{
              height: 1, background: KX2.glassLine, margin: '8px 0',
            }} />
            <div style={{
              fontFamily: KX2.fontMono, fontSize: 10, color: KX2.ink3,
              letterSpacing: 0.6, textTransform: 'uppercase',
              padding: '0 10px 4px',
            }}>Threads</div>
          </div>
        )}
        {liveMode ? (
          <>
            {nowThread && (
              <ThreadRow key={nowThread.slug} thread={nowThread} dense
                active={nowThread.slug === activeSlug}
                onClick={() => onPick?.(nowThread.slug)} />
            )}
            {folderBuckets.map(({ folder, threads }) => (
              <FolderSection key={folder.id}
                folder={folder} threads={threads} dense
                activeSlug={activeSlug}
                menuThreadSlug={menuSlug}
                onMenuToggle={onMenuToggle}
                folders={liveFolders}
                onRenameThread={onRenameThread}
                onMoveThread={onMoveThread}
                onDeleteThread={onDeleteThread}
                onRenameFolder={onRenameFolder}
                onDeleteFolder={onDeleteFolder}
                onPickThread={(slug) => onPick?.(slug)}
              />
            ))}
            {rootThreads.length > 0 && (
              <div style={{ marginTop: folderBuckets.length ? 8 : 0 }}>
                {folderBuckets.length > 0 && (
                  <div style={{
                    fontFamily: KX2.fontMono, fontSize: 10, color: KX2.ink3,
                    letterSpacing: 0.6, textTransform: 'uppercase',
                    padding: '6px 10px 4px',
                  }}>Conversations</div>
                )}
                {rootThreads.map(t => (
                  <ThreadRow key={t.slug} thread={t} dense
                    active={t.slug === activeSlug}
                    onClick={() => onPick?.(t.slug)}
                    menuOpen={menuSlug === t.slug}
                    onMenuToggle={onMenuToggle}
                    folders={liveFolders}
                    onRename={onRenameThread}
                    onMove={onMoveThread}
                    onDelete={onDeleteThread}
                  />
                ))}
              </div>
            )}
          </>
        ) : (
          groups.map(g => {
            const items = sorted.filter(g.match);
            if (!items.length) return null;
            return (
              <div key={g.id} style={{ marginTop: g.id === 'now' ? 0 : 12 }}>
                {g.label && (
                  <div style={{
                    fontFamily: KX2.fontMono, fontSize: 10, color: KX2.ink3,
                    letterSpacing: 0.6, textTransform: 'uppercase',
                    padding: '4px 10px 4px',
                  }}>{g.label}</div>
                )}
                {items.map(t => (
                  <ThreadRow key={t.slug} thread={t} dense
                    active={t.slug === activeSlug}
                    onClick={() => onPick?.(t.slug)}
                  />
                ))}
              </div>
            );
          })
        )}
      </div>
    </div>
  );
}

// ============ SCOPED HEADER (shown above chat when not on "now") ============
function KxV2ScopedHeader({ thread, onBack }) {
  if (!thread || thread.slug === 'now' || thread.slug === '__draft__') return null;
  const seenKey = `kxv2_scoped_seen_${thread.slug}`;
  const [showHint, setShowHint] = React.useState(() => {
    try { return !localStorage.getItem(seenKey); } catch { return true; }
  });
  React.useEffect(() => {
    // Show again if you switch into a thread you haven't seen this scope tip for
    try {
      setShowHint(!localStorage.getItem(seenKey));
    } catch {}
  }, [thread.slug, seenKey]);
  const dismiss = () => {
    setShowHint(false);
    try { localStorage.setItem(seenKey, '1'); } catch {}
  };
  return (
    <>
      <div style={{
        ...kx2Glass({ borderRadius: 0, borderTop: 'none', borderLeft: 'none', borderRight: 'none' }),
        padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 10,
        flexShrink: 0,
      }}>
        <button onClick={onBack} aria-label="Back to Now" style={{
          background: 'transparent', border: 'none', color: KX2.ink2,
          cursor: 'pointer', padding: 4,
          display: 'flex', alignItems: 'center', gap: 4,
          fontFamily: KX2.fontUI, fontSize: 12.5,
        }}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
          Now
        </button>
        <span style={{ color: KX2.inkDim }}>·</span>
        <ThreadIcon kind={thread.kind} size={24} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 14, fontWeight: 600, color: KX2.ink, lineHeight: 1.2,
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{thread.name}</div>
          <div style={{ fontFamily: KX2.fontMono, fontSize: 10.5, color: KX2.ink3,
            marginTop: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{thread.summary}</div>
        </div>
      </div>
      {showHint && (
        <div style={{
          margin: '8px 14px 0', padding: '8px 12px',
          ...kx2GlassThin({ borderRadius: 10 }),
          background: KX2.brandSoft,
          border: `1px solid ${KX2.brand}`,
          display: 'flex', alignItems: 'center', gap: 10,
        }}>
          <span style={{ fontSize: 14 }}>💡</span>
          <div style={{ flex: 1, fontSize: 12.5, color: KX2.ink, lineHeight: 1.45 }}>
            Just <b>{thread.name}</b> now. Tap <b>Now</b> at the top to see everything again.
          </div>
          <button onClick={dismiss} aria-label="Got it" style={{
            background: 'transparent', border: 'none', color: KX2.ink2,
            cursor: 'pointer', fontFamily: KX2.fontMono, fontSize: 11, padding: 4,
          }}>Got it</button>
        </div>
      )}
    </>
  );
}

Object.assign(window, {
  KXV2_THREADS, kxv2SortThreads, kxv2FilterMessages,
  KxV2ThreadsSheet, KxV2ThreadsRail, KxV2ScopedHeader, ThreadIcon, ThreadRow,
  ThreadRowMenu, FolderSection,
});
