// Marco-Polliio — UI components
// Wordmark, TopNav, Icons, FeaturePanel, CommentsPanel, modals.

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

// ── 5 face avatars (real image files served from assets/faces/) ───────
const FACES = [
  { id: 'bj1',        src: 'assets/faces/bj1.png' },
  { id: 'bj2',        src: 'assets/faces/bj2.png' },
  { id: 'worm',       src: 'assets/faces/worm.png' },
  { id: 'ye_stretch', src: 'assets/faces/ye_stretch.png' },
  { id: 'ye_king',    src: 'assets/faces/ye_king.png' },
];

// Renders any face id as a circular thumbnail — object-fit cover so wildly
// different aspect-ratio source images all read consistently.
function FaceIcon({ face, size = 32 }) {
  const f = (typeof face === 'string') ? FACES.find(x => x.id === face) : face;
  if (!f) return null;
  return (
    <span
      className="face-img"
      style={{
        width: size,
        height: size,
        display: 'inline-block',
        borderRadius: '50%',
        overflow: 'hidden',
        background: '#1f2230',
        flexShrink: 0,
      }}
      aria-hidden="true"
    >
      <img
        src={f.src}
        alt=""
        draggable={false}
        style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
      />
    </span>
  );
}

function FacePicker({ value, onChange }) {
  return (
    <div className="face-picker">
      {FACES.map((f) => (
        <button
          key={f.id} type="button"
          className="face-option" data-active={value === f.id}
          onClick={() => onChange(f.id)}
          aria-label={f.id} title={f.id}
        >
          <FaceIcon face={f} size={56} />
        </button>
      ))}
    </div>
  );
}


// ── tiny inline SVG icons (lucide-flavored, 1.5px stroke) ──────────────
function Icon({ name, size = 16 }) {
  const common = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round' };
  const paths = {
    plus:    <><path d="M12 5v14"/><path d="M5 12h14"/></>,
    trash:   <><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></>,
    image:   <><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="M21 15l-5-5L5 21"/></>,
    mic:     <><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10v2a7 7 0 0 0 14 0v-2"/><path d="M12 19v3"/></>,
    pencil:  <><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4z"/></>,
    send:    <><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></>,
    close:   <><path d="M18 6L6 18"/><path d="M6 6l12 12"/></>,
    thumbUp: <><path d="M7 10v12"/><path d="M15 5.88L14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 7A2 2 0 0 1 17.5 21H7"/><path d="M3 10h4v12H3z"/></>,
    thumbDn: <><path d="M17 14V2"/><path d="M9 18.12L10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-7A2 2 0 0 1 6.5 3H17"/><path d="M21 14h-4V2h4z"/></>,
    minus:   <><path d="M5 12h14"/></>,
    logout:  <><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/></>,
    eraser:  <><path d="M20 20H8.5l-5.5-5.5a2.83 2.83 0 0 1 0-4l9-9a2.83 2.83 0 0 1 4 0l5 5a2.83 2.83 0 0 1 0 4L11 20"/></>,
    edit:    <><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></>,
  };
  return <svg {...common}>{paths[name]}</svg>;
}

// ── Animated marco↔polio dance ────────────────────────────────────────
// Two words bounce across the same horizontal margins:
//   marco starts left, goes L→R, holds, then R→L home.
//   polio starts right, goes R→L (criss-crosses marco), holds, then L→R home.
// Vertical bounces are phase-offset so when one is on the ground the other is in flight.
function PollHero() {
  const trackRef = React.useRef(null);
  const marcoRef = React.useRef(null);
  const polioRef = React.useRef(null);

  React.useEffect(() => {
    const update = () => {
      if (!trackRef.current) return;
      const tw = trackRef.current.clientWidth;
      const margin = 24;
      if (marcoRef.current) {
        const mw = marcoRef.current.getBoundingClientRect().width;
        marcoRef.current.style.setProperty('--bd', Math.max(0, tw - mw - margin) + 'px');
      }
      if (polioRef.current) {
        const pw = polioRef.current.getBoundingClientRect().width;
        polioRef.current.style.setProperty('--bd', Math.max(0, tw - pw - margin) + 'px');
      }
    };
    update();
    const ro = new ResizeObserver(update);
    ro.observe(trackRef.current);
    window.addEventListener('resize', update);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(update);
    return () => { ro.disconnect(); window.removeEventListener('resize', update); };
  }, []);

  return (
    <div className="poll-track" ref={trackRef} aria-label="marco pollio">
      <span className="anim-word anim-marco" ref={marcoRef}>
        <span className="anim-y">marco</span>
      </span>
      <span className="anim-word anim-polio" ref={polioRef}>
        <span className="anim-y">pollio</span>
      </span>
    </div>
  );
}
function Wordmark() {
  return (
    <div className="wordmark-stack" aria-label="schedii · marco-poliio">
      <div className="wm wm-schedii">sched<span className="rd">ii</span></div>
      <div className="wm wm-marco">marco<span className="rd">-</span>pol<span className="rd">ii</span>o</div>
    </div>
  );
}

// ── TopNav ────────────────────────────────────────────────────────────
function TopNav({ user, onLogout }) {
  return (
    <header className="topnav">
      <div className="topnav-left" />
      <Wordmark />
      <div className="topnav-right">
        {user && (
          <>
            <div className="user-pill" data-role={user.role}>
              {user.avatar
                ? <span className="avatar avatar-face"><FaceIcon face={user.avatar} size={22} /></span>
                : <span className="avatar">{user.name[0].toUpperCase()}</span>}
              <span>{user.name}</span>
              <span className="role-tag">{user.role}</span>
            </div>
            <button className="btn btn-ghost" type="button" onClick={onLogout} aria-label="Reset identity" title="Reset identity (clears this browser)">
              <Icon name="logout" size={14} />
            </button>
          </>
        )}
      </div>
    </header>
  );
}

// ── Identity screen — one combined first-screen picker ───────────────
// Name + face + optional admin password. Backend issues a token; we never
// store passwords client-side. Fresh browser session = repick.
function IdentityScreen({ onSubmit, initialError }) {
  const [name, setName] = useState('');
  const [avatar, setAvatar] = useState(FACES[0].id);
  const [password, setPassword] = useState('');
  const [showPw, setShowPw] = useState(false);
  const [err, setErr] = useState(initialError || '');
  const [busy, setBusy] = useState(false);

  const submit = async (e) => {
    e.preventDefault();
    if (!name.trim()) { setErr('Pick a name.'); return; }
    setBusy(true);
    setErr('');
    const msg = await onSubmit({ name: name.trim(), avatar, password: password.trim() });
    setBusy(false);
    if (msg) setErr(msg);
  };

  return (
    <div className="login-screen">
      <form className="login-card signup-card" onSubmit={submit}>
        <Wordmark />
        <p className="tagline">name · face · password</p>
        <label htmlFor="iname">Name</label>
        <input id="iname" type="text" value={name}
          onChange={(e) => { setName(e.target.value); setErr(''); }}
          placeholder="how others see you" autoFocus maxLength={40} />
        <label>Face</label>
        <FacePicker value={avatar} onChange={setAvatar} />
        <label htmlFor="ipw" style={{ marginTop: 4 }}>
          Password
          <span style={{ opacity: 0.55, fontWeight: 400, marginLeft: 6, fontSize: 11 }}>required · ask the dev for it</span>
        </label>
        <div style={{ display: 'flex', gap: 6 }}>
          <input id="ipw" type={showPw ? 'text' : 'password'} value={password}
            onChange={(e) => { setPassword(e.target.value); setErr(''); }}
            placeholder="(blank)" autoComplete="off"
            style={{ flex: 1 }} />
          <button type="button" className="btn btn-ghost" onClick={() => setShowPw(!showPw)}
            style={{ padding: '6px 10px', fontSize: 11 }}>
            {showPw ? 'hide' : 'show'}
          </button>
        </div>
        <div className="err">{err}</div>
        <button className="btn btn-primary" type="submit" disabled={busy}
          style={{ width: '100%', justifyContent: 'center', padding: '12px', marginTop: 10 }}>
          {busy ? 'Entering…' : 'Enter'}
        </button>
      </form>
    </div>
  );
}

// ── helpers ────────────────────────────────────────────────────────────
function relTime(ts) {
  const s = Math.floor((Date.now() - ts) / 1000);
  if (s < 60) return 'just now';
  if (s < 3600) return Math.floor(s/60) + 'm ago';
  if (s < 86400) return Math.floor(s/3600) + 'h ago';
  if (s < 604800) return Math.floor(s/86400) + 'd ago';
  return new Date(ts).toLocaleDateString();
}

function fileToDataURL(file) {
  return new Promise((res, rej) => {
    const r = new FileReader();
    r.onload = () => res(r.result);
    r.onerror = rej;
    r.readAsDataURL(file);
  });
}

// Compress an image data URL to keep localStorage usage reasonable.
async function compressImage(dataUrl, maxDim = 1280, quality = 0.82) {
  return new Promise((res) => {
    const img = new Image();
    img.onload = () => {
      const scale = Math.min(1, maxDim / Math.max(img.width, img.height));
      const w = Math.round(img.width * scale);
      const h = Math.round(img.height * scale);
      const c = document.createElement('canvas');
      c.width = w; c.height = h;
      c.getContext('2d').drawImage(img, 0, 0, w, h);
      res(c.toDataURL('image/jpeg', quality));
    };
    img.onerror = () => res(dataUrl);
    img.src = dataUrl;
  });
}

// ── Normalize feature.media (accept old feature.images for back-compat) ─
// Backend stores media as a flat String[] of data URLs. In-memory the
// modal also accepts {type, src} objects so we have one shape for editing.
// These helpers unify reads across both shapes.
function getMedia(feature) {
  if (feature.media && feature.media.length) return feature.media;
  if (feature.images && feature.images.length) {
    return feature.images.map((src) => ({ type: 'image', src }));
  }
  return [];
}
function mediaSrc(m)     { return (m && typeof m === 'object') ? m.src : m; }
function mediaIsVideo(m) {
  if (m && typeof m === 'object' && m.type) return m.type === 'video';
  const s = mediaSrc(m) || '';
  return s.startsWith('data:video/');
}

// ── Media panel (left — images / videos stacked) ─────────────────────
function MediaPanel({ feature, canManage, onDelete, onZoom }) {
  const media = getMedia(feature);
  return (
    <div className="media-panel">
      {canManage && (
        <div className="admin-actions">
          <button className="btn btn-danger" type="button" onClick={() => onDelete(feature.id)} aria-label="Delete">
            <Icon name="trash" size={14} />
          </button>
        </div>
      )}
      {media.length === 0 ? (
        <div className="media-empty">no media yet — {canManage ? 'edit this card to add images or video.' : 'the author hasn’t attached anything.'}</div>
      ) : (
        <div className="media-stack">
          {media.map((m, i) => {
            const src = mediaSrc(m);
            return (
              <div className="media-item" key={i}>
                {mediaIsVideo(m)
                  ? <video src={src} controls playsInline preload="metadata" />
                  : <img src={src} alt="" onClick={() => onZoom(src)} />}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ── Description card (right — top) ────────────────────────────────────
function DescCard({ feature, canManage, onEdit }) {
  return (
    <div className="desc-card">
      {canManage && (
        <div className="admin-actions">
          <button className="btn btn-ghost" type="button" onClick={() => onEdit(feature)} aria-label="Edit">
            <Icon name="edit" size={14} />
          </button>
        </div>
      )}
      <div className="meta">
        <span className="dot" />
        <span>feature · {feature.id.slice(-4)}</span>
        <span>·</span>
        <span>{relTime(feature.ts)}</span>
      </div>
      <h2>{feature.title}</h2>
      {feature.description && <p className="desc">{feature.description}</p>}
    </div>
  );
}

// ── Vote bar ──────────────────────────────────────────────────────────
function VoteBar({ feature, onVote }) {
  const { up = 0, down = 0, neutral = 0 } = feature.voteCounts || {};
  const mine = feature.myVote; // 'up' | 'down' | 'neutral' | null
  const pick = (kind) => onVote(feature.id, mine === kind ? null : kind);
  return (
    <div className="vote-bar">
      <button className="vote-btn" type="button" data-kind="up" data-active={mine === 'up'} onClick={() => pick('up')} title="Thumbs up">
        <Icon name="thumbUp" size={18} />
        <span className="num">{up}</span>
      </button>
      <button className="vote-btn" type="button" data-kind="neutral" data-active={mine === 'neutral'} onClick={() => pick('neutral')} title="Neutral">
        <Icon name="minus" size={18} />
        <span className="num">{neutral}</span>
      </button>
      <button className="vote-btn" type="button" data-kind="down" data-active={mine === 'down'} onClick={() => pick('down')} title="Thumbs down">
        <Icon name="thumbDn" size={18} />
        <span className="num">{down}</span>
      </button>
    </div>
  );
}

// ── Comment item (with expand for long text) ──────────────────────────
const COMMENT_TRUNCATE = 260;
function CommentItem({ comment, canManage, onEdit, onDelete, onZoom }) {
  const [open, setOpen] = useState(false);
  const [editing, setEditing] = useState(false);
  const [editText, setEditText] = useState(comment.text || '');
  const long = !editing && comment.text && comment.text.length > COMMENT_TRUNCATE;
  const shown = !long || open ? comment.text : comment.text.slice(0, COMMENT_TRUNCATE).trimEnd() + '…';
  const startEdit  = () => { setEditText(comment.text || ''); setEditing(true); };
  const cancelEdit = () => { setEditing(false); setEditText(comment.text || ''); };
  const saveEdit   = () => {
    if (editText.trim() === (comment.text || '').trim()) { setEditing(false); return; }
    onEdit(comment.id, { text: editText.trim() });
    setEditing(false);
  };
  return (
    <div className="comment" data-role={comment.role}>
      {comment.avatar
        ? <span className="av av-face"><FaceIcon face={comment.avatar} size={26} /></span>
        : <div className="av">{comment.name[0].toUpperCase()}</div>}
      <div className="body">
        <div className="top-line">
          <span className="name">{comment.name}</span>
          <span className="role">{comment.role}</span>
          <span className="ts">
            {relTime(comment.ts)}
            {comment.edited && <span className="edited"> · edited</span>}
          </span>
          {canManage && !editing && (
            <span className="comment-actions">
              <button className="comment-action" type="button" onClick={startEdit} aria-label="Edit comment" title="Edit">
                <Icon name="edit" size={11} />
              </button>
              <button className="comment-action comment-action-danger" type="button" onClick={() => onDelete(comment.id)} aria-label="Delete comment" title="Delete">
                <Icon name="trash" size={11} />
              </button>
            </span>
          )}
        </div>
        {editing ? (
          <div className="comment-edit">
            <textarea value={editText}
              onChange={(e) => setEditText(e.target.value)}
              onKeyDown={(e) => {
                if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') saveEdit();
                if (e.key === 'Escape') cancelEdit();
              }}
              autoFocus />
            <div className="comment-edit-actions">
              <button className="btn btn-ghost" type="button" onClick={cancelEdit}>cancel</button>
              <button className="btn btn-primary" type="button" onClick={saveEdit}>save</button>
            </div>
          </div>
        ) : comment.text && (
          <div className="text">
            {shown}
            {long && (
              <button className="expand-btn" type="button" onClick={() => setOpen(!open)}>
                {open ? 'show less' : 'show more'}
              </button>
            )}
          </div>
        )}
        {comment.audio && (
          <div className="audio-chip">
            <Icon name="mic" size={11} />
            <span>voice · transcribed</span>
          </div>
        )}
        {comment.attachments?.length > 0 && (
          <div className="attachments">
            {comment.attachments.map((src, i) => (
              <img key={i} src={src} alt="" onClick={() => onZoom(src)} />
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// ── Composer (textarea + mic + image + draw + send) ───────────────────
function Composer({ user, onSend, onRequestDraw }) {
  const [text, setText] = useState('');
  const [pending, setPending] = useState([]);  // [{src, kind: 'img'|'draw'}]
  const [audio, setAudio] = useState(false);   // marks this comment as voice-derived
  const [listening, setListening] = useState(false);
  const recogRef = useRef(null);
  const fileRef = useRef(null);
  const baseTextRef = useRef('');

  const addDrawing = useCallback((src) => {
    setPending((p) => [...p, { src, kind: 'draw' }]);
  }, []);

  const startListening = () => {
    const Rec = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!Rec) { alert('Speech recognition not supported in this browser. Try Chrome.'); return; }
    if (listening) { recogRef.current?.stop(); return; }
    const r = new Rec();
    r.continuous = true;
    r.interimResults = true;
    r.lang = navigator.language || 'en-US';
    baseTextRef.current = text ? text + (text.endsWith(' ') ? '' : ' ') : '';
    r.onresult = (ev) => {
      let interim = '', finalTxt = '';
      for (let i = ev.resultIndex; i < ev.results.length; i++) {
        const t = ev.results[i][0].transcript;
        if (ev.results[i].isFinal) finalTxt += t; else interim += t;
      }
      setText(baseTextRef.current + finalTxt + interim);
      if (finalTxt) baseTextRef.current += finalTxt;
    };
    r.onend = () => { setListening(false); setAudio(true); };
    r.onerror = () => { setListening(false); };
    recogRef.current = r;
    r.start();
    setListening(true);
  };

  const onPickImage = async (e) => {
    const files = [...e.target.files];
    e.target.value = '';
    for (const f of files) {
      const raw = await fileToDataURL(f);
      const small = await compressImage(raw);
      setPending((p) => [...p, { src: small, kind: 'img' }]);
    }
  };

  const send = () => {
    if (!text.trim() && pending.length === 0) return;
    recogRef.current?.stop();
    onSend({
      text: text.trim(),
      attachments: pending.map((p) => p.src),
      audio,
    });
    setText('');
    setPending([]);
    setAudio(false);
    baseTextRef.current = '';
  };

  return (
    <div className="composer">
      <textarea
        placeholder={`comment as ${user.name}… ⌘+Enter to send`}
        value={text}
        onChange={(e) => { setText(e.target.value); baseTextRef.current = e.target.value; }}
        onKeyDown={(e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') send(); }}
      />
      {pending.length > 0 && (
        <div className="pending-attach">
          {pending.map((a, i) => (
            <div className="att" key={i}>
              <img src={a.src} alt="" />
              <button className="x" type="button" onClick={() => setPending((p) => p.filter((_, j) => j !== i))}>×</button>
            </div>
          ))}
        </div>
      )}
      <div className="composer-toolbar">
        <button className="tool-btn" type="button" data-listening={listening} onClick={startListening} title={listening ? 'Stop listening' : 'Voice → text'}>
          <Icon name="mic" size={15} />
        </button>
        <button className="tool-btn" type="button" onClick={() => fileRef.current?.click()} title="Attach image">
          <Icon name="image" size={15} />
        </button>
        <button className="tool-btn" type="button" onClick={() => onRequestDraw(addDrawing)} title="Draw on a canvas">
          <Icon name="pencil" size={15} />
        </button>
        <input ref={fileRef} type="file" accept="image/*" multiple style={{ display: 'none' }} onChange={onPickImage} />
        <div className="spacer" />
        <button className="btn btn-primary" type="button" onClick={send} disabled={!text.trim() && pending.length === 0}>
          <Icon name="send" size={13} /> Send
        </button>
      </div>
    </div>
  );
}

// ── Reaction bar (stackable emoji) ────────────────────────────────────
const REACTION_PICKS = ['🔥', '💡', '❤️', '👀', '🚀', '❓', '🤔', '✋'];

function ReactionBar({ feature, onReact }) {
  const [pickerOpen, setPickerOpen] = useState(false);
  const reactions = feature.reactions || {};
  // Render existing reactions in REACTION_PICKS order so chips don't reshuffle.
  const active = REACTION_PICKS.filter((e) => reactions[e]?.count > 0);
  const unused = REACTION_PICKS.filter((e) => !reactions[e]?.count);
  return (
    <div className="reaction-bar">
      {active.map((emoji) => {
        const cell = reactions[emoji];
        return (
          <button
            key={emoji} type="button"
            className="reaction-chip"
            data-mine={!!cell.mine}
            onClick={() => onReact(feature.id, emoji)}
            title={cell.mine ? 'click to remove your reaction' : 'click to add this reaction'}
          >
            <span className="emoji">{emoji}</span>
            <span className="count">{cell.count}</span>
          </button>
        );
      })}
      <div className="reaction-add">
        <button type="button" className="reaction-chip reaction-add-btn"
          onClick={() => setPickerOpen((o) => !o)} aria-label="Add reaction">
          + <span className="emoji" aria-hidden>😀</span>
        </button>
        {pickerOpen && (
          <div className="reaction-picker" onMouseLeave={() => setPickerOpen(false)}>
            {unused.length === 0 ? (
              <div className="reaction-picker-empty">all in use — click an active one to remove yours</div>
            ) : unused.map((emoji) => (
              <button key={emoji} type="button" className="reaction-picker-emoji"
                onClick={() => { onReact(feature.id, emoji); setPickerOpen(false); }}>
                {emoji}
              </button>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// ── Comment thread (full-width, below the feature) ───────────────────
// Vote/reaction bars live in the feature top section; this is purely
// the conversation thread + composer.
function CommentThread({ feature, user, onComment, onZoom, onRequestDraw, canCommentManage, onEditComment, onDeleteComment }) {
  return (
    <div className="comment-thread">
      <div className="comments-list">
        {(feature.comments || []).map((c) => (
          <CommentItem
            key={c.id}
            comment={c}
            canManage={canCommentManage(c)}
            onEdit={(cid, data) => onEditComment(feature.id, cid, data)}
            onDelete={(cid) => onDeleteComment(feature.id, cid)}
            onZoom={(src) => onZoom(src, feature.id)}
          />
        ))}
        {(!feature.comments || feature.comments.length === 0) && (
          <div className="thread-empty">no comments yet — start the conversation.</div>
        )}
      </div>
      <Composer
        user={user}
        onRequestDraw={onRequestDraw}
        onSend={(c) => onComment(feature.id, c)}
      />
    </div>
  );
}

// ── Feature row ───────────────────────────────────────────────────────
// Vertical: top section = media + desc + votes + reactions side-by-side.
// Bottom section = full-width comment thread (conversation under each feature).
function FeatureRow({ feature, user, canManage, canCommentManage, onVote, onReact, onComment, onDelete, onEdit, onEditComment, onDeleteComment, onZoom, onRequestDraw }) {
  const onZoomWithId = (src) => onZoom(src, feature.id);
  return (
    <article className="feature-row" data-screen-label={`feature-${feature.id.slice(-4)}`}>
      <div className="feature-top">
        <MediaPanel feature={feature} canManage={canManage} onDelete={onDelete} onZoom={onZoomWithId} />
        <div className="feature-meta">
          <DescCard feature={feature} canManage={canManage} onEdit={onEdit} />
          <VoteBar feature={feature} onVote={onVote} />
          <ReactionBar feature={feature} onReact={onReact} />
        </div>
      </div>
      <CommentThread
        feature={feature}
        user={user}
        onComment={onComment}
        onZoom={onZoom}
        onRequestDraw={onRequestDraw}
        canCommentManage={canCommentManage}
        onEditComment={onEditComment}
        onDeleteComment={onDeleteComment}
      />
    </article>
  );
}

// ── Add/Edit feature modal ────────────────────────────────────────────
const VIDEO_MAX_BYTES = 5 * 1024 * 1024;
function FeatureModal({ initial, onClose, onSave, noun }) {
  const label = noun || 'feature';
  const [title, setTitle] = useState(initial?.title || '');
  const [desc, setDesc] = useState(initial?.description || '');
  const [media, setMedia] = useState(() => {
    if (initial?.media?.length) return initial.media;
    if (initial?.images?.length) return initial.images.map((src) => ({ type: 'image', src }));
    return [];
  });
  const fileRef = useRef(null);

  const onPick = async (e) => {
    const files = [...e.target.files];
    e.target.value = '';
    for (const f of files) {
      if (f.type.startsWith('image/')) {
        const raw = await fileToDataURL(f);
        const small = await compressImage(raw);
        setMedia((p) => [...p, { type: 'image', src: small }]);
      } else if (f.type.startsWith('video/')) {
        if (f.size > VIDEO_MAX_BYTES) {
          alert(`Video too large (${(f.size/1024/1024).toFixed(1)} MB). Max ${VIDEO_MAX_BYTES/1024/1024} MB so it fits in browser storage.`);
          continue;
        }
        const url = await fileToDataURL(f);
        setMedia((p) => [...p, { type: 'video', src: url }]);
      }
    }
  };

  const save = () => {
    if (!title.trim()) return;
    // Backend stores media as String[]; flatten {type,src} object entries
    // to bare data URLs. The data URL itself carries the mime type so
    // render-time detection (mediaIsVideo) still works.
    const flat = media.map((m) => mediaSrc(m)).filter(Boolean);
    onSave({ title: title.trim(), description: desc.trim(), media: flat });
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <h3>{initial ? `Edit ${label}` : `New ${label}`}</h3>
          <button className="btn btn-ghost" type="button" onClick={onClose}><Icon name="close" size={14} /></button>
        </div>
        <label htmlFor="ft">Title</label>
        <input id="ft" type="text" value={title} onChange={(e) => setTitle(e.target.value)} placeholder={label === 'suggestion' ? 'e.g. Add a quick-react keyboard shortcut' : 'e.g. Bulk schedule from CSV'} autoFocus />
        <label htmlFor="fd">Description</label>
        <textarea id="fd" value={desc} onChange={(e) => setDesc(e.target.value)} placeholder="Why this matters, what it should do…" />
        <label>Media · images or short video (≤ 5 MB)</label>
        <button className="btn btn-secondary" type="button" onClick={() => fileRef.current?.click()}>
          <Icon name="image" size={13} /> Upload media
        </button>
        <input ref={fileRef} type="file" accept="image/*,video/*" multiple style={{ display: 'none' }} onChange={onPick} />
        {media.length > 0 && (
          <div className="image-thumb-grid">
            {media.map((m, i) => {
              const src = mediaSrc(m);
              return (
                <div className="thumb" key={i}>
                  {mediaIsVideo(m)
                    ? <video src={src} muted />
                    : <img src={src} alt="" />}
                  <button className="x" type="button" onClick={() => setMedia((p) => p.filter((_, j) => j !== i))}>×</button>
                </div>
              );
            })}
          </div>
        )}
        <div className="modal-actions">
          <button className="btn btn-ghost" type="button" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary" type="button" onClick={save} disabled={!title.trim()}>
            {initial ? 'Save' : 'Publish'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Drawing modal ─────────────────────────────────────────────────────
function DrawModal({ onClose, onSave }) {
  const canvasRef = useRef(null);
  const [color, setColor] = useState('#06b6d4');
  const [size, setSize] = useState(3);
  const drawing = useRef(false);
  const last = useRef({ x: 0, y: 0 });
  const colors = ['#1e293b', '#06b6d4', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6'];

  useEffect(() => {
    const c = canvasRef.current;
    const ctx = c.getContext('2d');
    ctx.fillStyle = '#f8fafc';
    ctx.fillRect(0, 0, c.width, c.height);
  }, []);

  const pos = (e) => {
    const c = canvasRef.current;
    const rect = c.getBoundingClientRect();
    const x = ((e.clientX ?? e.touches?.[0]?.clientX) - rect.left) * (c.width / rect.width);
    const y = ((e.clientY ?? e.touches?.[0]?.clientY) - rect.top) * (c.height / rect.height);
    return { x, y };
  };
  const down = (e) => { drawing.current = true; last.current = pos(e); };
  const move = (e) => {
    if (!drawing.current) return;
    const c = canvasRef.current;
    const ctx = c.getContext('2d');
    const p = pos(e);
    ctx.strokeStyle = color;
    ctx.lineWidth = size;
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(last.current.x, last.current.y);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
    last.current = p;
  };
  const up = () => { drawing.current = false; };

  const clear = () => {
    const c = canvasRef.current;
    const ctx = c.getContext('2d');
    ctx.fillStyle = '#f8fafc';
    ctx.fillRect(0, 0, c.width, c.height);
  };
  const save = () => {
    const url = canvasRef.current.toDataURL('image/png');
    onSave(url);
    onClose();
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal draw-modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <h3>Draw</h3>
          <button className="btn btn-ghost" type="button" onClick={onClose}><Icon name="close" size={14} /></button>
        </div>
        <div className="draw-canvas-wrap">
          <canvas
            ref={canvasRef}
            width={1280}
            height={720}
            onMouseDown={down}
            onMouseMove={move}
            onMouseUp={up}
            onMouseLeave={up}
            onTouchStart={(e) => { e.preventDefault(); down(e); }}
            onTouchMove={(e) => { e.preventDefault(); move(e); }}
            onTouchEnd={up}
          />
        </div>
        <div className="draw-tools">
          <span style={{ fontSize: 12, color: 'var(--color-muted-foreground)' }}>colors</span>
          {colors.map((c) => (
            <button key={c} className="swatch" data-active={color === c} style={{ background: c }} onClick={() => setColor(c)} aria-label={c} />
          ))}
          <span style={{ fontSize: 12, color: 'var(--color-muted-foreground)', marginLeft: 12 }}>size</span>
          <input type="range" min="1" max="20" value={size} onChange={(e) => setSize(+e.target.value)} />
          <span style={{ fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 12, color: 'var(--color-muted-foreground)' }}>{size}px</span>
          <div style={{ flex: 1 }} />
          <button className="btn btn-ghost" type="button" onClick={clear}><Icon name="eraser" size={13} /> Clear</button>
        </div>
        <div className="modal-actions">
          <button className="btn btn-ghost" type="button" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary" type="button" onClick={save}>Attach drawing</button>
        </div>
      </div>
    </div>
  );
}

// ── Lightbox (with optional Annotate action) ──────────────────────────
function Lightbox({ lightbox, onClose, onAnnotate }) {
  if (!lightbox) return null;
  const src = typeof lightbox === 'string' ? lightbox : lightbox.src;
  const featureId = typeof lightbox === 'string' ? null : lightbox.featureId;
  return (
    <div className="lightbox" onClick={onClose}>
      <img src={src} alt="" onClick={(e) => e.stopPropagation()} />
      {onAnnotate && featureId && (
        <div className="lightbox-actions" onClick={(e) => e.stopPropagation()}>
          <button className="btn btn-primary" type="button" onClick={() => onAnnotate(src, featureId)}>
            <Icon name="pencil" size={13} /> annotate · share your thoughts
          </button>
        </div>
      )}
    </div>
  );
}

// ── Annotate modal (skeleton — pen / highlight / stamps on top of image) ─
function AnnotateModal({ imageSrc, onClose, onSave }) {
  const canvasRef = React.useRef(null);
  const [tool, setTool] = useState('pen');
  const [color, setColor] = useState('#ef4444');
  const [size, setSize] = useState(4);
  const drawing = React.useRef(false);
  const last = React.useRef({ x: 0, y: 0 });

  // Load image to canvas on mount
  const drawBaseImage = () => new Promise((res) => {
    const c = canvasRef.current;
    const img = new Image();
    img.onload = () => {
      const maxDim = 1280;
      const scale = Math.min(1, maxDim / Math.max(img.naturalWidth, img.naturalHeight));
      c.width  = Math.round(img.naturalWidth * scale);
      c.height = Math.round(img.naturalHeight * scale);
      const ctx = c.getContext('2d');
      ctx.fillStyle = '#0a0a14';
      ctx.fillRect(0, 0, c.width, c.height);
      ctx.drawImage(img, 0, 0, c.width, c.height);
      res();
    };
    img.onerror = () => res();
    img.src = imageSrc;
  });
  React.useEffect(() => { drawBaseImage(); /* eslint-disable-next-line */ }, [imageSrc]);

  const pos = (e) => {
    const c = canvasRef.current;
    const rect = c.getBoundingClientRect();
    const x = ((e.clientX ?? e.touches?.[0]?.clientX) - rect.left) * (c.width / rect.width);
    const y = ((e.clientY ?? e.touches?.[0]?.clientY) - rect.top)  * (c.height / rect.height);
    return { x, y };
  };
  const placeStamp = (p) => {
    const ctx = canvasRef.current.getContext('2d');
    const stampSize = size * 8 + 20;
    ctx.save();
    ctx.font = `bold ${stampSize}px sans-serif`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = color;
    ctx.shadowColor = 'rgba(0,0,0,0.45)';
    ctx.shadowBlur = 6;
    const glyph =
      tool === 'stamp-check' ? '✓' :
      tool === 'stamp-x'     ? '✕' :
      tool === 'stamp-q'     ? '?' :
      tool === 'stamp-star'  ? '★' : '';
    ctx.fillText(glyph, p.x, p.y);
    ctx.restore();
  };
  const down = (e) => {
    const p = pos(e);
    if (tool.startsWith('stamp-')) { placeStamp(p); return; }
    drawing.current = true;
    last.current = p;
  };
  const move = (e) => {
    if (!drawing.current || tool.startsWith('stamp-')) return;
    const ctx = canvasRef.current.getContext('2d');
    const p = pos(e);
    ctx.strokeStyle = color;
    ctx.lineWidth = tool === 'highlight' ? size * 4 : size;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.globalAlpha = tool === 'highlight' ? 0.35 : 1;
    ctx.beginPath();
    ctx.moveTo(last.current.x, last.current.y);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
    ctx.globalAlpha = 1;
    last.current = p;
  };
  const up = () => { drawing.current = false; };
  const clear = () => { drawBaseImage(); };
  const save = () => {
    const url = canvasRef.current.toDataURL('image/png');
    onSave(url);
    onClose();
  };

  const tools = [
    { id: 'pen',         label: 'pen' },
    { id: 'highlight',   label: 'highlight' },
    { id: 'stamp-check', label: '✓ check' },
    { id: 'stamp-x',     label: '✕ x' },
    { id: 'stamp-q',     label: '? mark' },
    { id: 'stamp-star',  label: '★ star' },
  ];
  const colors = ['#ef4444', '#4ade80', '#fbbf24', '#3b82f6', '#ffffff', '#1e293b'];

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal annotate-modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <h3>annotate · show me your thoughts</h3>
          <button className="btn btn-ghost" type="button" onClick={onClose}><Icon name="close" size={14} /></button>
        </div>
        <div className="annotate-canvas-wrap">
          <canvas
            ref={canvasRef}
            onMouseDown={down} onMouseMove={move} onMouseUp={up} onMouseLeave={up}
            onTouchStart={(e) => { e.preventDefault(); down(e); }}
            onTouchMove={(e) => { e.preventDefault(); move(e); }}
            onTouchEnd={up}
          />
        </div>
        <div className="annotate-tools">
          <div className="tool-row">
            <span className="tool-row-label">tool</span>
            {tools.map((t) => (
              <button key={t.id} className="tool-chip" data-active={tool === t.id} type="button" onClick={() => setTool(t.id)}>
                {t.label}
              </button>
            ))}
          </div>
          <div className="tool-row">
            <span className="tool-row-label">color</span>
            {colors.map((c) => (
              <button key={c} className="swatch" data-active={color === c} type="button" style={{ background: c }} onClick={() => setColor(c)} aria-label={c} />
            ))}
          </div>
          <div className="tool-row">
            <span className="tool-row-label">size</span>
            <input type="range" min="2" max="14" value={size} onChange={(e) => setSize(+e.target.value)} />
            <span style={{ fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 12, color: 'var(--color-muted-foreground)' }}>{size}px</span>
            <button className="btn btn-ghost" type="button" onClick={clear} style={{ marginLeft: 'auto' }}><Icon name="eraser" size={13} /> reset</button>
          </div>
        </div>
        <div className="modal-actions">
          <button className="btn btn-ghost" type="button" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary" type="button" onClick={save}>Post annotation</button>
        </div>
      </div>
    </div>
  );
}


// ── Tabs (clean: tabs left · contextual actions right) ───────────────
function Tabs({ active, onChange, canCreateFeature, onNewFeature, onNewSuggestion }) {
  return (
    <div className="tab-switcher" role="tablist" aria-label="Page mode">
      <div className="tab-group">
        <button role="tab" className="tab-pill" data-tab="poll" data-active={active === 'poll'}
          aria-selected={active === 'poll'} type="button" onClick={() => onChange('poll')}>
          <span className="tab-dot" />
          <span className="tab-label">feature poll</span>
        </button>
        <button role="tab" className="tab-pill" data-tab="suggestions" data-active={active === 'suggestions'}
          aria-selected={active === 'suggestions'} type="button" onClick={() => onChange('suggestions')}>
          <span className="tab-dot" />
          <span className="tab-label">suggestions</span>
        </button>
      </div>
      <div className="tab-actions">
        {active === 'poll' && canCreateFeature && (
          <button className="btn-create btn-create-feature" type="button" onClick={onNewFeature}>
            <Icon name="plus" size={13} /> new feature
          </button>
        )}
        {active === 'suggestions' && (
          <button className="btn-create btn-create-suggestion" type="button" onClick={onNewSuggestion}>
            <Icon name="plus" size={13} /> new suggestion
          </button>
        )}
      </div>
    </div>
  );
}

Object.assign(window, {
  Wordmark, TopNav, IdentityScreen, FeatureRow, FeatureModal, DrawModal,
  Lightbox, AnnotateModal, Icon, PollHero, Tabs, FaceIcon, FACES,
  relTime, fileToDataURL, compressImage,
});
