// fc-expanded.jsx — ExpandedNodeView + PollSection

const { useState, useRef, useEffect, useCallback } = React;

// ── PollSection ────────────────────────────────────────────────────────────
function PollSection({ node, readOnly, onUpdatePoll }) {
  const poll     = node.poll || null;
  // Linked nodes share the same poll (poll is a synced field), so scope the
  // per-browser "already voted" guard to the link group when there is one —
  // otherwise the same user could vote once per linked copy and inflate counts.
  const voteScope = node.linkGroupId || node.id;
  const localKey = `fc_poll_vote_${voteScope}`;
  const myVote   = (() => { try { return localStorage.getItem(localKey); } catch { return null; } })();
  const hasVoted = !!myVote;

  const [editing,   setEditing]   = useState(false);
  const [draftQ,    setDraftQ]    = useState('');
  const [draftOpts, setDraftOpts] = useState([]);

  function startCreate() {
    setDraftQ('');
    setDraftOpts([{ id: uid('opt'), text: '' }, { id: uid('opt'), text: '' }]);
    setEditing(true);
  }

  function startEdit() {
    if (!poll) return;
    setDraftQ(poll.question);
    setDraftOpts(poll.options.map(o => ({ ...o })));
    setEditing(true);
  }

  function savePoll() {
    const question = draftQ.trim();
    const options  = draftOpts.filter(o => o.text.trim()).map(o => ({ ...o, text: o.text.trim() }));
    if (!question || options.length < 2) return;
    onUpdatePoll(node.id, { question, options, votes: poll?.votes || {} });
    setEditing(false);
  }

  function deletePoll() {
    if (!confirm('Delete this poll? All votes will be lost.')) return;
    onUpdatePoll(node.id, null);
    try { localStorage.removeItem(localKey); } catch {}
  }

  function castVote(optionId) {
    if (hasVoted || !poll) return;
    const newVotes = { ...(poll.votes || {}), [optionId]: ((poll.votes || {})[optionId] || 0) + 1 };
    onUpdatePoll(node.id, { ...poll, votes: newVotes });
    try { localStorage.setItem(localKey, optionId); } catch {}
  }

  function retractVote() {
    if (!hasVoted || !poll) return;
    const prev = (poll.votes || {})[myVote] || 0;
    const newVotes = { ...(poll.votes || {}), [myVote]: Math.max(0, prev - 1) };
    onUpdatePoll(node.id, { ...poll, votes: newVotes });
    try { localStorage.removeItem(localKey); } catch {}
  }

  const totalVotes = poll ? Object.values(poll.votes || {}).reduce((s, v) => s + v, 0) : 0;

  if (editing) {
    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <FcLabel>{poll ? 'Edit Poll' : 'Create Poll'}</FcLabel>
          <button onClick={() => setEditing(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text2)', fontSize: 18, lineHeight: 1 }}>×</button>
        </div>
        <FcInput value={draftQ} autoFocus onChange={e => setDraftQ(e.target.value)} placeholder="Poll question…" />
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {draftOpts.map((opt, i) => (
            <div key={opt.id} style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
              <span style={{ fontSize: 11, color: 'var(--text2)', width: 16, textAlign: 'right', flexShrink: 0 }}>{i + 1}.</span>
              <FcInput
                value={opt.text}
                onChange={e => setDraftOpts(prev => prev.map((o, j) => j === i ? { ...o, text: e.target.value } : o))}
                onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); setDraftOpts(prev => [...prev, { id: uid('opt'), text: '' }]); } }}
                placeholder={`Option ${i + 1}…`}
                style={{ flex: 1 }}
              />
              {draftOpts.length > 2 && (
                <button onClick={() => setDraftOpts(prev => prev.filter((_, j) => j !== i))}
                  style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#f87171', fontSize: 16, flexShrink: 0 }}>×</button>
              )}
            </div>
          ))}
          <button
            onClick={() => setDraftOpts(prev => [...prev, { id: uid('opt'), text: '' }])}
            style={{ alignSelf: 'flex-start', background: 'none', border: '1px dashed var(--border)', borderRadius: 7, padding: '4px 12px', fontSize: 11, color: 'var(--text2)', cursor: 'pointer', fontFamily: 'inherit' }}
          >+ Add option</button>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <FcBtn primary disabled={!draftQ.trim() || draftOpts.filter(o => o.text.trim()).length < 2} onClick={savePoll} style={{ flex: 1 }}>Save Poll</FcBtn>
          <FcBtn onClick={() => setEditing(false)}>Cancel</FcBtn>
        </div>
      </div>
    );
  }

  if (!poll) {
    if (readOnly) return null;
    return <FcBtn onClick={startCreate} style={{ width: '100%', textAlign: 'center' }}>◉ Add poll to this node</FcBtn>;
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 }}>
        <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)', lineHeight: 1.4 }}>◉ {poll.question}</div>
        {!readOnly && (
          <div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
            <button onClick={startEdit} style={{ background: 'none', border: '1px solid var(--border)', borderRadius: 6, padding: '3px 8px', fontSize: 10, cursor: 'pointer', color: 'var(--text2)', fontFamily: 'inherit' }}>Edit</button>
            <button onClick={deletePoll} style={{ background: 'none', border: '1px solid #f8717140', borderRadius: 6, padding: '3px 8px', fontSize: 10, cursor: 'pointer', color: '#f87171', fontFamily: 'inherit' }}>Delete</button>
          </div>
        )}
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
        {poll.options.map(opt => {
          const votes  = (poll.votes || {})[opt.id] || 0;
          const pct    = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
          const isMyV  = myVote === opt.id;
          const canVote = !hasVoted;
          return (
            <div key={opt.id}
              onClick={() => canVote && castVote(opt.id)}
              style={{
                position: 'relative', overflow: 'hidden',
                background: isMyV ? 'var(--accent-muted)' : 'var(--bg)',
                border: `1.5px solid ${isMyV ? 'var(--accent)' : 'var(--border)'}`,
                borderRadius: 9, padding: '9px 12px',
                cursor: canVote ? 'pointer' : 'default', transition: 'all 0.12s',
              }}
              onMouseEnter={e => { if (canVote) e.currentTarget.style.borderColor = 'var(--accent)'; }}
              onMouseLeave={e => { if (canVote && !isMyV) e.currentTarget.style.borderColor = 'var(--border)'; }}
            >
              {hasVoted && (
                <div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: `${pct}%`, background: isMyV ? 'var(--accent)' : 'var(--border)', opacity: 0.18, transition: 'width 0.4s', borderRadius: 8 }} />
              )}
              <div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
                <span style={{
                  fontSize: 13,
                  // Force full --text colour (no fading) — earlier version
                  // relied on inherited muted text in dark mode which made
                  // unvoted options nearly unreadable. Bump weight a notch
                  // too so they read clearly on both themes.
                  color: 'var(--text)',
                  fontWeight: isMyV ? 700 : 600,
                  display: 'flex', alignItems: 'center', gap: 6,
                }}>
                  {isMyV && <span style={{ color: 'var(--accent)', fontSize: 11 }}>✓</span>}
                  {opt.text}
                </span>
                {hasVoted && (
                  <span style={{ fontSize: 11, fontWeight: 700, color: isMyV ? 'var(--accent)' : 'var(--text2)', flexShrink: 0 }}>
                    {pct}% <span style={{ fontWeight: 400, color: 'var(--text2)' }}>({votes})</span>
                  </span>
                )}
              </div>
            </div>
          );
        })}
      </div>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: 11, color: 'var(--text2)' }}>
        <span>{totalVotes} vote{totalVotes !== 1 ? 's' : ''}</span>
        {hasVoted
          ? <button onClick={retractVote} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: 'var(--text2)', fontFamily: 'inherit', textDecoration: 'underline', padding: 0 }}>Remove my vote</button>
          : <span style={{ fontStyle: 'italic' }}>Click an option to vote</span>
        }
      </div>
    </div>
  );
}

// ── ImageGrid ──────────────────────────────────────────────────────────────
function ImageGrid({ images, readOnly, nodeId, onAdd, onRemove, onOpenLightbox }) {
  const fileRef = useRef(null);
  const [dragOver, setDragOver] = useState(false);

  async function handleFiles(fileList) {
    const files = [...(fileList || [])];
    for (const file of files) {
      if (!file.type.startsWith('image/')) continue;
      try {
        const dataUrl = (typeof readAndCompressImageFile === 'function')
          ? await readAndCompressImageFile(file)
          : await new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result);
            reader.onerror = () => reject(new Error('Read failed'));
            reader.readAsDataURL(file);
          });
        onAdd(nodeId, dataUrl);
      } catch (err) {
        if (err?.message === 'too_large') alert(`"${file.name}" too large (max 4 MB)`);
      }
    }
  }

  function handleDrop(e) {
    e.preventDefault(); setDragOver(false);
    const files = [...(e.dataTransfer.files || [])].filter(f => f.type.startsWith('image/'));
    if (files.length) handleFiles(files);
  }

  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
        <FcLabel>Images ({images.length})</FcLabel>
        {!readOnly && (
          <>
            <FcBtn onClick={() => fileRef.current?.click()}>+ Add image</FcBtn>
            <input type="file" accept="image/*" multiple ref={fileRef} style={{ display: 'none' }}
              onChange={e => { handleFiles(e.target.files); e.target.value = ''; }} />
          </>
        )}
      </div>

      {images.length > 0 ? (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))', gap: 8 }}>
          {images.map((src, i) => (
            <div key={i}
              style={{ position: 'relative', paddingBottom: '75%', borderRadius: 8, overflow: 'hidden', background: 'var(--bg2)', border: '1px solid var(--border)', cursor: 'zoom-in' }}
              onClick={() => onOpenLightbox(i)}>
              <img src={src} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} alt="" />
              {!readOnly && (
                <button
                  onClick={e => { e.stopPropagation(); if (confirm('Remove image?')) onRemove(nodeId, i); }}
                  style={{ position: 'absolute', top: 4, right: 4, background: 'rgba(0,0,0,0.7)', color: '#fff', border: 'none', borderRadius: 99, width: 22, height: 22, cursor: 'pointer', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
                >×</button>
              )}
            </div>
          ))}
          {/* Drop zone tile */}
          {!readOnly && (
            <div
              onDragOver={e => { e.preventDefault(); setDragOver(true); }}
              onDragLeave={() => setDragOver(false)}
              onDrop={handleDrop}
              onClick={() => fileRef.current?.click()}
              style={{
                paddingBottom: '75%', borderRadius: 8, position: 'relative',
                border: `1.5px dashed ${dragOver ? 'var(--accent)' : 'var(--border)'}`,
                background: dragOver ? 'var(--accent-muted)' : 'transparent',
                cursor: 'pointer', transition: 'all 0.15s',
              }}>
              <div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', color: 'var(--text2)', fontSize: 18 }}>+</div>
            </div>
          )}
        </div>
      ) : (
        <div
          onDragOver={e => { e.preventDefault(); setDragOver(true); }}
          onDragLeave={() => setDragOver(false)}
          onDrop={handleDrop}
          onClick={() => !readOnly && fileRef.current?.click()}
          style={{
            border: `1.5px dashed ${dragOver ? 'var(--accent)' : 'var(--border)'}`,
            background: dragOver ? 'var(--accent-muted)' : 'transparent',
            borderRadius: 10, padding: '28px 16px', textAlign: 'center',
            color: 'var(--text2)', fontSize: 12, lineHeight: 1.6,
            cursor: readOnly ? 'default' : 'pointer', transition: 'all 0.15s',
          }}
        >
          {readOnly ? 'No images attached' : (dragOver ? 'Drop to upload' : 'Drop images here, paste (Ctrl+V), or click to upload')}
        </div>
      )}
    </div>
  );
}

// ── Lightbox ───────────────────────────────────────────────────────────────
function Lightbox({ images, startIndex, onClose }) {
  const [idx, setIdx] = useState(startIndex);

  useEffect(() => {
    function onKey(e) {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowRight' || e.key === 'ArrowDown') setIdx(i => Math.min(images.length - 1, i + 1));
      if (e.key === 'ArrowLeft'  || e.key === 'ArrowUp')   setIdx(i => Math.max(0, i - 1));
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [images.length, onClose]);

  if (!images.length) return null;

  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.92)', display: 'grid', placeItems: 'center', zIndex: 300, cursor: 'zoom-out' }}>
      <div style={{ position: 'relative', maxWidth: '92vw', maxHeight: '92vh', display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'center' }}
        onClick={e => e.stopPropagation()}>
        <img src={images[idx]} style={{ maxWidth: '88vw', maxHeight: '80vh', borderRadius: 10, boxShadow: '0 12px 40px rgba(0,0,0,0.5)', objectFit: 'contain' }} alt="" />
        <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
          <button
            onClick={() => setIdx(i => Math.max(0, i - 1))}
            disabled={idx === 0}
            style={{ background: 'rgba(255,255,255,0.12)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: 8, padding: '6px 16px', color: '#fff', cursor: idx === 0 ? 'not-allowed' : 'pointer', opacity: idx === 0 ? 0.35 : 1, fontFamily: 'inherit', fontSize: 14 }}
          >‹ Prev</button>
          <span style={{ color: 'rgba(255,255,255,0.6)', fontSize: 12 }}>{idx + 1} / {images.length}</span>
          <button
            onClick={() => setIdx(i => Math.min(images.length - 1, i + 1))}
            disabled={idx === images.length - 1}
            style={{ background: 'rgba(255,255,255,0.12)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: 8, padding: '6px 16px', color: '#fff', cursor: idx === images.length - 1 ? 'not-allowed' : 'pointer', opacity: idx === images.length - 1 ? 0.35 : 1, fontFamily: 'inherit', fontSize: 14 }}
          >Next ›</button>
        </div>
        <button onClick={onClose} style={{ position: 'absolute', top: -36, right: 0, background: 'rgba(255,255,255,0.12)', border: 'none', borderRadius: 8, padding: '4px 10px', color: '#fff', cursor: 'pointer', fontSize: 18, fontFamily: 'inherit' }}>✕</button>
      </div>
    </div>
  );
}

// ── PollsModal ─────────────────────────────────────────────────────────────
// Lists every poll across every chart, split into "To Vote" and "Voted" tabs.
// Clicking a poll opens its node in the expanded view.
function PollsModal({ project, onClose, onOpenNode }) {
  const [tab, setTab] = useState('todo'); // 'todo' | 'voted'

  // Gather every poll-bearing node from every chart
  const allPolls = [];
  (project?.charts || []).forEach(c => {
    (c.nodes || []).forEach(n => {
      if (n.poll) allPolls.push({ chart: c, node: n });
    });
  });

  function myVoteFor(node) {
    // Mirror PollSection's link-group-aware vote scope.
    const scope = node.linkGroupId || node.id;
    try { return localStorage.getItem(`fc_poll_vote_${scope}`); } catch { return null; }
  }

  const todoPolls  = allPolls.filter(p => !myVoteFor(p.node));
  const votedPolls = allPolls.filter(p =>  myVoteFor(p.node));
  const list = tab === 'todo' ? todoPolls : votedPolls;

  return (
    <div
      style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', display: 'grid', placeItems: 'center', zIndex: 140, backdropFilter: 'blur(6px)', padding: 20 }}
      onClick={e => { if (e.target === e.currentTarget) onClose(); }}
    >
      <div style={{
        background: 'var(--surface)', border: '1px solid var(--border)',
        borderRadius: 18, width: 'min(640px, 96vw)', maxHeight: '88vh',
        display: 'flex', flexDirection: 'column', overflow: 'hidden',
        boxShadow: '0 24px 64px rgba(0,0,0,0.4)',
      }}>
        {/* Header */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '14px 18px', borderBottom: '1px solid var(--border)' }}>
          <span style={{ fontSize: 16, fontWeight: 700, color: 'var(--text)' }}>◉ Polls</span>
          <span style={{ fontSize: 11, color: 'var(--text2)' }}>{allPolls.length} total</span>
          <span style={{ flex: 1 }} />
          <button onClick={onClose} style={{
            background: 'transparent', border: '1px solid var(--border)', borderRadius: 8,
            padding: '4px 10px', cursor: 'pointer', color: 'var(--text)', fontFamily: 'inherit', fontSize: 12,
          }}>✕</button>
        </div>

        {/* Tab strip */}
        <div style={{ display: 'flex', gap: 0, padding: '0 18px', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
          {[
            { id: 'todo',  label: 'To Vote', count: todoPolls.length,  color: '#f59e0b' },
            { id: 'voted', label: 'Voted',   count: votedPolls.length, color: '#22c55e' },
          ].map(t => {
            const active = tab === t.id;
            return (
              <button key={t.id} onClick={() => setTab(t.id)} style={{
                background: 'transparent', border: 'none', cursor: 'pointer',
                padding: '11px 14px', fontSize: 12, fontWeight: 600,
                color: active ? 'var(--text)' : 'var(--text2)',
                borderBottom: `2px solid ${active ? t.color : 'transparent'}`,
                fontFamily: 'inherit', display: 'flex', alignItems: 'center', gap: 7,
                marginBottom: -1,
              }}>
                {t.label}
                <span style={{
                  fontSize: 10, fontWeight: 700, padding: '1px 7px', borderRadius: 99,
                  background: active ? t.color : 'var(--bg)',
                  color: active ? '#fff' : 'var(--text2)',
                  border: active ? 'none' : '1px solid var(--border)',
                }}>{t.count}</span>
              </button>
            );
          })}
        </div>

        {/* List */}
        <div style={{ flex: 1, overflowY: 'auto', padding: 16, display: 'flex', flexDirection: 'column', gap: 10 }}>
          {list.length === 0 ? (
            <div style={{ textAlign: 'center', padding: '48px 16px', color: 'var(--text2)', lineHeight: 1.7 }}>
              <div style={{ fontSize: 32, opacity: 0.25, marginBottom: 8 }}>◉</div>
              <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)' }}>
                {tab === 'todo' ? 'No polls waiting' : 'No polls voted yet'}
              </div>
              <div style={{ fontSize: 11, marginTop: 4 }}>
                {tab === 'todo' ? 'You\'ve voted on every poll. Nice.' : 'Cast a vote and it\'ll show up here.'}
              </div>
            </div>
          ) : list.map(({ chart, node }) => {
            const poll = node.poll;
            const myVote = myVoteFor(node);
            const myOpt = myVote ? poll.options.find(o => o.id === myVote) : null;
            const totalVotes = Object.values(poll.votes || {}).reduce((s, v) => s + v, 0);
            const leading = poll.options.reduce((best, o) => {
              const v = (poll.votes || {})[o.id] || 0;
              return v > best.v ? { o, v } : best;
            }, { o: null, v: -1 });
            return (
              <button
                key={node.id}
                onClick={() => { onOpenNode(chart.id, node.id); onClose(); }}
                style={{
                  textAlign: 'left', background: 'var(--bg)', border: '1px solid var(--border)',
                  borderRadius: 12, padding: '12px 14px', cursor: 'pointer',
                  fontFamily: 'inherit', color: 'var(--text)',
                  display: 'flex', flexDirection: 'column', gap: 8, transition: 'all 0.12s',
                }}
                onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--accent)'; e.currentTarget.style.background = 'var(--bg2)'; }}
                onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.background = 'var(--bg)'; }}
              >
                {/* Question + breadcrumb */}
                <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 }}>
                  <div style={{ minWidth: 0, flex: 1 }}>
                    <div style={{ fontSize: 9, fontWeight: 700, color: 'var(--text2)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 4 }}>
                      {chart.title || 'Untitled chart'} <span style={{ opacity: 0.5 }}>›</span> {node.label || 'Untitled node'}
                    </div>
                    <div style={{ fontSize: 13, fontWeight: 700, lineHeight: 1.35, color: 'var(--text)' }}>◉ {poll.question}</div>
                  </div>
                  {myOpt ? (
                    <span style={{ fontSize: 9, fontWeight: 700, color: '#22c55e', background: '#22c55e22', padding: '3px 9px', borderRadius: 99, whiteSpace: 'nowrap', flexShrink: 0 }}>
                      ✓ Voted
                    </span>
                  ) : (
                    <span style={{ fontSize: 9, fontWeight: 700, color: '#f59e0b', background: '#f59e0b22', padding: '3px 9px', borderRadius: 99, whiteSpace: 'nowrap', flexShrink: 0 }}>
                      Vote now
                    </span>
                  )}
                </div>

                {/* Bottom row: option count + your vote / leading */}
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, fontSize: 11, color: 'var(--text2)' }}>
                  <span>{poll.options.length} option{poll.options.length !== 1 ? 's' : ''} · {totalVotes} vote{totalVotes !== 1 ? 's' : ''}</span>
                  {myOpt && (
                    <span style={{ color: 'var(--accent)' }}>Your pick: <strong style={{ color: 'var(--text)', fontWeight: 600 }}>{myOpt.text}</strong></span>
                  )}
                  {!myOpt && leading.v > 0 && (
                    <span>Leading: <strong style={{ color: 'var(--text)', fontWeight: 600 }}>{leading.o.text}</strong> · {leading.v}</span>
                  )}
                </div>
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// ── ExpandedNodeView ───────────────────────────────────────────────────────
function ExpandedNodeView({ node, chart, nodeTypes, statuses, readOnly, onClose,
  onUpdateNode, onUpdatePoll, onDeleteNode, onOpenSubChart, onCreateSubChart,
  onAddImage, onRemoveImage }) {

  if (!node) return null;

  const type    = nodeTypes.find(t => t.id === node.typeId) || { color: '#94a3b8', name: 'Generic' };
  const [lightboxIdx, setLightboxIdx] = useState(null);
  const [detailsMode, setDetailsMode] = useState('edit'); // 'edit' | 'preview'
  const [tagInput, setTagInput] = useState('');

  // ── Local mirrors for fast-typing-friendly text fields ──
  // Controlled inputs were reverting on remote sync echos. We keep a local
  // copy that only adopts the incoming value when:
  //   (a) the node id changes, or
  //   (b) the incoming value differs AND the field isn't currently focused.
  // Edits flush to the parent on each keystroke (same as before) so live-sync
  // and undo/redo still work — but our display value never goes backwards mid-typing.
  const [localLabel,   setLocalLabel]   = useState(node.label   || '');
  const [localNotes,   setLocalNotes]   = useState(node.notes   || '');
  const [localDetails, setLocalDetails] = useState(node.details || '');
  const labelRef   = useRef(null);
  const notesRef   = useRef(null);
  const detailsRef = useRef(null);
  const lastNodeIdRef = useRef(node.id);

  useEffect(() => {
    const isNewNode = lastNodeIdRef.current !== node.id;
    lastNodeIdRef.current = node.id;
    const incomingLabel   = node.label   || '';
    const incomingNotes   = node.notes   || '';
    const incomingDetails = node.details || '';
    const active = document.activeElement;
    if (isNewNode || (incomingLabel   !== localLabel   && active !== labelRef.current))   setLocalLabel(incomingLabel);
    if (isNewNode || (incomingNotes   !== localNotes   && active !== notesRef.current))   setLocalNotes(incomingNotes);
    if (isNewNode || (incomingDetails !== localDetails && active !== detailsRef.current)) setLocalDetails(incomingDetails);
  }, [node.id, node.label, node.notes, node.details]);

  function upNode(changes) { onUpdateNode(node.id, changes); }

  function addTag(e) {
    if (e.key !== 'Enter' && e.key !== ',') return;
    e.preventDefault();
    const val = tagInput.trim().replace(/^#/, '').toLowerCase();
    if (!val) return;
    const cur = node.tags || [];
    if (!cur.includes(val)) upNode({ tags: [...cur, val] });
    setTagInput('');
  }

  async function handlePaste(e) {
    const items = e.clipboardData?.items || [];
    for (const item of items) {
      if (item.type.startsWith('image/')) {
        const blob = item.getAsFile();
        if (!blob) continue;
        try {
          const dataUrl = (typeof readAndCompressImageFile === 'function')
            ? await readAndCompressImageFile(blob)
            : await new Promise((resolve, reject) => {
              const reader = new FileReader();
              reader.onload = () => resolve(reader.result);
              reader.onerror = () => reject(new Error('Read failed'));
              reader.readAsDataURL(blob);
            });
          onAddImage(node.id, dataUrl);
        } catch (err) {
          if (err?.message === 'too_large') alert('Pasted image too large (max 4 MB)');
        }
      }
    }
  }

  useEffect(() => {
    function onKey(e) { if (e.key === 'Escape' && lightboxIdx === null) onClose(); }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose, lightboxIdx]);

  const detailsWords = (localDetails || '').trim().split(/\s+/).filter(Boolean).length;
  const detailsChars = (localDetails || '').length;

  // Simple markdown-style preview: bold **x**, italic *x*, headings ## x, code `x`
  function renderPreview(text) {
    if (!text) return <span style={{ color: 'var(--text2)', fontStyle: 'italic' }}>No details yet.</span>;
    const lines = text.split('\n');
    return lines.map((line, i) => {
      let el;
      if (/^## (.+)/.test(line)) el = <strong style={{ fontSize: 15, display: 'block', marginTop: 8 }}>{line.replace(/^## /, '')}</strong>;
      else if (/^# (.+)/.test(line)) el = <strong style={{ fontSize: 17, display: 'block', marginTop: 10 }}>{line.replace(/^# /, '')}</strong>;
      else if (/^- (.+)/.test(line)) el = <div style={{ paddingLeft: 14 }}>• {line.replace(/^- /, '')}</div>;
      else if (line.trim() === '') el = <div style={{ height: 8 }} />;
      else el = <div>{line}</div>;
      return <React.Fragment key={i}>{el}</React.Fragment>;
    });
  }

  return (
    <div
      style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.65)', display: 'grid', placeItems: 'center', zIndex: 150, backdropFilter: 'blur(8px)', padding: 20 }}
      onClick={e => { if (e.target === e.currentTarget) onClose(); }}
      onPaste={handlePaste}
    >
      <div
        style={{
          background: 'var(--surface)', border: '1px solid var(--border)',
          borderTop: `4px solid ${type.color}`,
          borderRadius: 18, width: 'min(980px, 96vw)', maxHeight: '94vh',
          display: 'flex', flexDirection: 'column', overflow: 'hidden',
          boxShadow: '0 28px 80px rgba(0,0,0,0.5)',
        }}
        onClick={e => e.stopPropagation()}
      >
        {/* Header */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 18px', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
          <span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: type.color, background: type.color + '18', padding: '3px 8px', borderRadius: 99 }}>{type.name}</span>
          {(node.tags || []).map((tag, i) => (
            <span key={i} style={{ fontSize: 10, fontWeight: 600, color: 'var(--text2)', background: 'var(--bg)', padding: '2px 8px', borderRadius: 99, border: '1px solid var(--border)' }}>#{tag}</span>
          ))}
          <span style={{ flex: 1 }} />
          <span style={{ fontSize: 10, color: 'var(--text2)' }}>Double-click node · Enter to open</span>
          <FcBtn onClick={onClose} title="Close (Esc)" style={{ padding: '4px 10px' }}>✕</FcBtn>
        </div>

        {/* Body */}
        <div style={{ flex: 1, overflowY: 'auto', padding: '20px 22px', display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 264px', gap: 24 }}>

          {/* Left column */}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 18, minWidth: 0 }}>

            {/* Title */}
            <input
              ref={labelRef}
              value={localLabel}
              disabled={readOnly}
              onChange={e => { setLocalLabel(e.target.value); upNode({ label: e.target.value }); }}
              placeholder="Title…"
              style={{
                background: 'transparent', border: 'none', outline: 'none',
                fontSize: 26, fontWeight: 800, color: 'var(--text)', fontFamily: 'inherit',
                padding: '2px 0', borderBottom: '1.5px solid transparent', width: '100%',
              }}
              onFocus={e => { e.currentTarget.style.borderBottomColor = 'var(--border)'; }}
              onBlur={e => { e.currentTarget.style.borderBottomColor = 'transparent'; }}
            />

            {/* Summary / notes */}
            <div>
              <FcLabel>Summary</FcLabel>
              <textarea
                ref={notesRef}
                value={localNotes} disabled={readOnly} rows={2}
                placeholder="One-line summary…"
                onChange={e => { setLocalNotes(e.target.value); upNode({ notes: e.target.value }); }}
                style={{ ...fcInputStyle, resize: 'vertical', minHeight: 64, lineHeight: 1.5 }}
              />
            </div>

            {/* Details with preview toggle */}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                <FcLabel>Details</FcLabel>
                <div style={{ display: 'flex', gap: 2 }}>
                  {['edit', 'preview'].map(m => (
                    <button key={m} onClick={() => setDetailsMode(m)} style={{
                      background: detailsMode === m ? 'var(--accent)' : 'transparent',
                      border: `1px solid ${detailsMode === m ? 'var(--accent)' : 'var(--border)'}`,
                      borderRadius: 6, padding: '2px 8px', fontSize: 10, fontWeight: 600,
                      cursor: 'pointer', color: detailsMode === m ? '#fff' : 'var(--text2)',
                      fontFamily: 'inherit', textTransform: 'capitalize',
                    }}>{m}</button>
                  ))}
                </div>
              </div>

              {detailsMode === 'edit' ? (
                <textarea
                  ref={detailsRef}
                  value={localDetails}
                  disabled={readOnly}
                  placeholder="Write detailed notes here. Supports # headings, - lists, **bold**. Paste screenshots to attach."
                  onChange={e => { setLocalDetails(e.target.value); upNode({ details: e.target.value }); }}
                  style={{
                    ...fcInputStyle, resize: 'vertical', minHeight: 200,
                    lineHeight: 1.65, fontSize: 13, padding: '12px 14px',
                    fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
                  }}
                />
              ) : (
                <div style={{
                  ...fcInputStyle, minHeight: 200, lineHeight: 1.65, fontSize: 13,
                  padding: '12px 14px', color: 'var(--text)', cursor: 'text',
                  overflowY: 'auto',
                }} onClick={() => setDetailsMode('edit')}>
                  {renderPreview(localDetails)}
                </div>
              )}

              <div style={{ fontSize: 10, color: 'var(--text2)', textAlign: 'right', opacity: 0.6 }}>
                {detailsWords} word{detailsWords !== 1 ? 's' : ''} · {detailsChars} char{detailsChars !== 1 ? 's' : ''}
              </div>
            </div>

            {/* Poll */}
            <div style={{ borderTop: '1px solid var(--border)', paddingTop: 16 }}>
              <FcLabel>Poll</FcLabel>
              <PollSection node={node} readOnly={readOnly} onUpdatePoll={onUpdatePoll} />
            </div>

            {/* Images */}
            <ImageGrid
              images={node.images || []}
              readOnly={readOnly}
              nodeId={node.id}
              onAdd={onAddImage}
              onRemove={onRemoveImage}
              onOpenLightbox={setLightboxIdx}
            />
          </div>

          {/* Right sidebar */}
          <aside style={{ display: 'flex', flexDirection: 'column', gap: 14, borderLeft: '1px solid var(--border)', paddingLeft: 20, minWidth: 0 }}>

            <div>
              <FcLabel>Type</FcLabel>
              <FcSelect value={node.typeId} disabled={readOnly} onChange={e => upNode({ typeId: e.target.value })}>
                {nodeTypes.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
              </FcSelect>
            </div>

            <div>
              <FcLabel>Status</FcLabel>
              <FcSelect value={node.status || statuses[0]?.id || 'todo'} disabled={readOnly} onChange={e => upNode({ status: e.target.value })}>
                {statuses.map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
              </FcSelect>
            </div>

            {/* Tags */}
            <div>
              <FcLabel>Tags</FcLabel>
              {!readOnly && (
                <FcInput
                  value={tagInput}
                  onChange={e => setTagInput(e.target.value)}
                  onKeyDown={addTag}
                  placeholder="tag, press Enter"
                  style={{ marginBottom: 6 }}
                />
              )}
              {(node.tags || []).length > 0 ? (
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                  {(node.tags || []).map((tag, i) => (
                    <span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 10, fontWeight: 600, color: type.color, background: type.color + '18', padding: '2px 8px', borderRadius: 99 }}>
                      #{tag}
                      {!readOnly && (
                        <button onClick={() => upNode({ tags: (node.tags || []).filter(t => t !== tag) })}
                          style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', color: type.color, fontSize: 13, lineHeight: 1, opacity: 0.7 }}>×</button>
                      )}
                    </span>
                  ))}
                </div>
              ) : <div style={{ fontSize: 11, color: 'var(--text2)', fontStyle: 'italic' }}>No tags</div>}
            </div>

            {/* Assignees */}
            <div>
              <FcLabel>Assignees</FcLabel>
              {(node.assignees || []).length > 0 ? (
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                  {(node.assignees || []).map((a, i) => (
                    <span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 11, background: 'var(--bg)', border: '1px solid var(--border)', color: 'var(--text2)', padding: '2px 8px', borderRadius: 99 }}>
                      {a}
                      {!readOnly && (
                        <button onClick={() => upNode({ assignees: (node.assignees || []).filter(x => x !== a) })}
                          style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', color: 'var(--text2)', fontSize: 13 }}>×</button>
                      )}
                    </span>
                  ))}
                </div>
              ) : <div style={{ fontSize: 11, color: 'var(--text2)', fontStyle: 'italic' }}>None</div>}
            </div>

            {/* Connections summary */}
            {chart && (
              <div>
                <FcLabel>Connections</FcLabel>
                <div style={{ fontSize: 11, color: 'var(--text2)', lineHeight: 1.8 }}>
                  <div>↑ {chart.edges.filter(e => e.to === node.id).length} parent{chart.edges.filter(e => e.to === node.id).length !== 1 ? 's' : ''}</div>
                  <div>↓ {chart.edges.filter(e => e.from === node.id).length} child{chart.edges.filter(e => e.from === node.id).length !== 1 ? 'ren' : ''}</div>
                </div>
              </div>
            )}

            {/* Sub-chart */}
            <div>
              <FcLabel>Sub-chart</FcLabel>
              {node.subChartId ? (
                <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                  <FcBtn primary onClick={() => { onOpenSubChart(node.subChartId, node.label); onClose(); }} style={{ width: '100%' }}>
                    Open sub-chart →
                  </FcBtn>
                  {!readOnly && (
                    <FcBtn danger onClick={() => upNode({ subChartId: null })} style={{ width: '100%' }}>Unlink</FcBtn>
                  )}
                </div>
              ) : (
                !readOnly && (
                  <FcBtn onClick={() => { onCreateSubChart(node.id, node.label); onClose(); }} style={{ width: '100%' }}>
                    + Create sub-chart
                  </FcBtn>
                )
              )}
            </div>

            <div style={{ flex: 1 }} />

            {!readOnly && node.id !== chart?.rootNodeId && (
              <FcBtn danger
                onClick={() => { if (confirm('Delete this node?')) { onDeleteNode(node.id); onClose(); } }}
                style={{ width: '100%' }}>
                Delete node
              </FcBtn>
            )}
          </aside>
        </div>
      </div>

      {/* Lightbox */}
      {lightboxIdx !== null && (
        <Lightbox
          images={node.images || []}
          startIndex={lightboxIdx}
          onClose={() => setLightboxIdx(null)}
        />
      )}
    </div>
  );
}

// ── Export ─────────────────────────────────────────────────────────────────
Object.assign(window, { ExpandedNodeView, PollSection, PollsModal });
