// Optional accounts and the shared Seamenese chat room.
const ACCOUNT_PROGRESS_KEYS = [
  'seamenese_dopamine_v2', 'seamenese_srs_v2', 'seamenese_solved_scenes_v3',
  'seamenese_selected_tier_v1', 'seamenese_auto_rotate_v1',
  'seamenese_mastered_words_v1', 'seamenese_words_mode_v1', 'seamenese_immersion_v1',
  'seamenese_scene_view_v1', 'seamenese_verb_streak_v1',
  'seamenese_activity_economy_v1', 'seamenese_scene_attempts_v1',
];
const ACCOUNT_CACHE_KEY = 'seamenese_account_cache_v1';
const GUEST_BACKUP_KEY = 'seamenese_guest_progress_v1';
const MIGRATION_BACKUP_KEY = 'seamenese_migration_backup_v1';
const MIGRATION_PROMPT_KEY = 'seamenese_migration_prompt_v1';

function guestMigrationSummary() {
  const read = (key, fallback) => {
    try { return JSON.parse(localStorage.getItem(key) || 'null') ?? fallback; } catch { return fallback; }
  };
  const record = value => value && typeof value === 'object' && !Array.isArray(value);
  let player = read('seamenese_dopamine_v2', null);
  if (!record(player)) player = read('seamenese_dopamine_v1', {});
  if (!record(player)) player = {};
  const positive = value => Number.isFinite(Number(value)) ? Math.max(0, Math.floor(Number(value))) : 0;
  const count = value => Array.isArray(value) ? new Set(value.filter(v => typeof v === 'string' && v.trim())).size : 0;
  const defaults = new Set(['theme_default', 'title_blober', 'np_default', 'frame_default', 'effect_sparkles', 'snd_default']);
  const unlocked = [...(Array.isArray(player.unlockedItems) ? player.unlockedItems : []), ...(Array.isArray(player.unlocked_perks) ? player.unlocked_perks : [])];
  const cosmetics = new Set(unlocked.filter(id => typeof id === 'string' && !defaults.has(id))).size;
  const deck = read('seamenese_srs_v2', {});
  const attempts = read('seamenese_scene_attempts_v1', {});
  const economy = read('seamenese_activity_economy_v1', {});
  const summary = {
    playerId: typeof player.playerId === 'string' && player.playerId ? player.playerId : 'legacy-device',
    points: positive(player.points),
    solvedScenes: count(read('seamenese_solved_scenes_v3', [])),
    masteredWords: count(read('seamenese_mastered_words_v1', [])),
    // Opening the Dojo seeds empty cards for every word. Only scheduled or
    // reviewed cards represent a learner's SRS progress.
    srsCards: record(deck) ? Object.values(deck).filter(card => record(card)
      && (positive(card.repetitions) || positive(card.dueAt) || positive(card.intervalDays) > 1)).length : 0,
    cosmetics,
  };
  summary.hasProgress = Boolean(summary.points || summary.solvedScenes || summary.masteredWords || summary.srsCards || cosmetics
    || String(player.username || '').trim() || positive(player.solvedCount) || positive(player.bestStreak) || positive(player.streak)
    || positive(player.perks?.streak_shields) || positive(player.perks?.double_xp_charges)
    || (record(attempts) && Object.values(attempts).some(a => Array.isArray(a?.clickedReplies) && a.clickedReplies.length))
    || (record(economy) && Array.isArray(economy.verbAttempts) && economy.verbAttempts.length));
  return summary;
}

function rememberMigrationPrompt(playerId) {
  try {
    const saved = JSON.parse(localStorage.getItem(MIGRATION_PROMPT_KEY) || '[]');
    const ids = Array.isArray(saved) ? saved.filter(id => typeof id === 'string') : [];
    localStorage.setItem(MIGRATION_PROMPT_KEY, JSON.stringify([...new Set([...ids, playerId])].slice(-20)));
  } catch {}
}

function shouldShowMigrationPrompt({ summary, ready, sessionChecked, account, blocked = false, handled = false }) {
  if (!summary?.hasProgress || !ready || !sessionChecked || account || blocked || handled) return false;
  try {
    // A cached account save needs sign-in or conflict resolution, not migration.
    if (localStorage.getItem(ACCOUNT_CACHE_KEY)) return false;
    const saved = JSON.parse(localStorage.getItem(MIGRATION_PROMPT_KEY) || '[]');
    return !Array.isArray(saved) || !saved.includes(summary.playerId);
  } catch { return false; }
}

function captureAccountProgress() {
  const progress = {};
  for (const key of ACCOUNT_PROGRESS_KEYS) {
    const value = localStorage.getItem(key);
    if (value !== null) progress[key] = value;
  }
  return progress;
}

function restoreAccountProgress(progress) {
  // A legacy save belongs to the old guest, never to a newly selected account.
  // A recovery copy is retained separately before any account transition.
  localStorage.removeItem('seamenese_dopamine_v1');
  for (const key of ACCOUNT_PROGRESS_KEYS) {
    if (Object.prototype.hasOwnProperty.call(progress, key)) localStorage.setItem(key, progress[key]);
    else localStorage.removeItem(key);
  }
}

function backupAccountMigration(mode) {
  const progress = captureAccountProgress();
  if (mode === 'register' || !localStorage.getItem(MIGRATION_BACKUP_KEY)) localStorage.setItem(MIGRATION_BACKUP_KEY, JSON.stringify({
    mode, progress, legacyPlayer: localStorage.getItem('seamenese_dopamine_v1'), savedAt: new Date().toISOString(),
  }));
  if (mode === 'login') localStorage.setItem(GUEST_BACKUP_KEY, JSON.stringify(progress));
  return progress;
}

function legacyLeaderboardFromBackup(backup, value) {
  if (!backup || backup.mode === 'login' || (backup.accountId && backup.accountId !== value.account.id)) return null;
  try {
    const old = JSON.parse(backup.progress?.seamenese_dopamine_v2 || backup.legacyPlayer || '{}');
    if (!old.playerId || old.playerId === value.account.playerId || (value.account.legacyPlayerIds || []).includes(old.playerId)) return null;
    if (!backup.mode) {
      const current = JSON.parse(value.progress.seamenese_dopamine_v2 || '{}');
      // Points can change through practice or store purchases after signup.
      if (!old.username || old.username !== current.username || Number(old.solvedCount || 0) > Number(current.solvedCount || 0)) return null;
    }
    return old.playerId;
  } catch { return null; }
}

function newAccountPasswordError(password, confirmation) {
  if ([...password].length < 15 || [...password].length > 128) return 'Use a password between 15 and 128 characters. A short phrase works well.';
  return password === confirmation ? '' : 'The passwords do not match.';
}

async function communityRequest(url, options = {}) {
  const response = await fetch(url, {
    cache: 'no-store', credentials: 'same-origin', ...options,
    headers: { 'Content-Type': 'application/json', ...options.headers },
  });
  const result = await response.json();
  if (!response.ok) throw Object.assign(new Error(result.error || 'Request failed.'), { status: response.status, result });
  return result;
}

function useAccountSync({ onRestore }) {
  const [account, setAccount] = React.useState(null);
  const [ready, setReady] = React.useState(false);
  const [sessionChecked, setSessionChecked] = React.useState(false);
  const [status, setStatus] = React.useState('guest');
  const [error, setError] = React.useState('');
  const current = React.useRef(null);
  const flight = React.useRef(null);
  const restoreRef = React.useRef(onRestore);
  restoreRef.current = onRestore;

  function cache(value, fingerprint) {
    localStorage.setItem(ACCOUNT_CACHE_KEY, JSON.stringify({ accountId: value.account.id, revision: value.revision, fingerprint }));
  }

  function adopt(value) {
    restoreAccountProgress(value.progress);
    restoreRef.current();
    const fingerprint = JSON.stringify(captureAccountProgress());
    current.current = { ...value, fingerprint, conflict: false };
    cache(value, fingerprint);
    setAccount(value.account);
    setStatus('saved');
    setError('');
    // Older registrations left a guest leaderboard row behind. Recover the
    // original public row ID from this browser's migration backup, never from
    // a guessed username, and repair it under the authenticated account.
    try {
      const backup = JSON.parse(localStorage.getItem(MIGRATION_BACKUP_KEY) || 'null');
      const legacyId = legacyLeaderboardFromBackup(backup, value);
      if (legacyId) {
        communityRequest('/api/account/reconcile-leaderboard', { method: 'POST', body: JSON.stringify({ legacy_player_id: legacyId }) }).catch(() => {});
      }
    } catch {}
  }

  async function sync() {
    if (flight.current) return flight.current;
    const active = current.current;
    if (!active || active.conflict) return;
    const progress = captureAccountProgress();
    const fingerprint = JSON.stringify(progress);
    if (fingerprint === active.fingerprint) return;
    setStatus('saving');
    flight.current = (async () => {
      try {
        const result = await communityRequest('/api/account/progress', {
          method: 'PUT', body: JSON.stringify({ revision: active.revision, progress }), keepalive: JSON.stringify(progress).length < 50_000,
        });
        active.revision = result.revision;
        active.fingerprint = fingerprint;
        cache(active, fingerprint);
        setStatus('saved'); setError('');
      } catch (err) {
        if (err.status === 409) { active.conflict = true; setStatus('conflict'); }
        else if (err.status === 401) { active.conflict = true; setStatus('signed out'); }
        else setStatus('offline');
        setError(err.message);
        throw err;
      } finally { flight.current = null; }
    })();
    return flight.current;
  }
  const syncRef = React.useRef(sync);
  syncRef.current = sync;

  React.useEffect(() => {
    let mounted = true;
    communityRequest('/api/account/session').then(value => {
      if (!mounted) return;
      setSessionChecked(true);
      if (!value.account) return;
      let saved;
      try { saved = JSON.parse(localStorage.getItem(ACCOUNT_CACHE_KEY) || 'null'); } catch {}
      const fingerprint = JSON.stringify(captureAccountProgress());
      if (saved?.accountId === value.account.id && saved.fingerprint !== fingerprint) {
        const conflict = saved.revision !== value.revision;
        current.current = { ...value, revision: saved.revision, fingerprint: saved.fingerprint, conflict };
        setAccount(value.account); setStatus(conflict ? 'conflict' : 'saving');
        if (conflict) setError('Both this device and your account have new progress. Choose which to keep.');
      } else adopt(value);
    }).catch(() => {
      // Continue playing locally. Never overwrite cached account progress on a failed boot.
      setError('Account sync is unavailable. Your progress is still saved on this device.');
    }).finally(() => { if (mounted) setReady(true); });
    const timer = setInterval(() => { if (mounted) syncRef.current().catch(() => {}); }, 2000);
    const flush = () => syncRef.current().catch(() => {});
    window.addEventListener('pagehide', flush);
    document.addEventListener('visibilitychange', flush);
    return () => { mounted = false; clearInterval(timer); window.removeEventListener('pagehide', flush); document.removeEventListener('visibilitychange', flush); };
  }, []);

  async function authenticate(mode, username, password) {
    const previous = current.current;
    if (previous && status !== 'signed out') throw new Error('Sign out before switching accounts.');
    if (previous && username.trim().toLowerCase() !== previous.account.username.toLowerCase()) throw new Error(`Sign in as ${previous.account.username} to keep your unsynced progress.`);
    // Save before the request: the server can succeed even if its response is
    // lost. Neither a timeout nor a later reload should discard the guest save.
    const progress = previous ? captureAccountProgress() : backupAccountMigration(mode);
    const value = await communityRequest(`/api/account/${mode}`, { method: 'POST', body: JSON.stringify({ username, password, ...(mode === 'register' ? { progress } : {}) }) });
    if (previous) {
      const conflict = previous.revision !== value.revision;
      current.current = { ...value, fingerprint: previous.fingerprint, conflict };
      setAccount(value.account); setStatus(conflict ? 'conflict' : 'saving'); setError('');
      return;
    }
    if (mode === 'register') {
      localStorage.removeItem(GUEST_BACKUP_KEY);
      try {
        const backup = JSON.parse(localStorage.getItem(MIGRATION_BACKUP_KEY) || '{}');
        localStorage.setItem(MIGRATION_BACKUP_KEY, JSON.stringify({ ...backup, accountId: value.account.id }));
      } catch {}
    }
    adopt(value);
  }

  async function logout() {
    if (current.current?.conflict) throw new Error('Resolve the progress conflict before signing out.');
    await sync();
    await communityRequest('/api/account/logout', { method: 'POST', body: '{}' });
    let guest = {};
    try { guest = JSON.parse(localStorage.getItem(GUEST_BACKUP_KEY) || '{}'); } catch {}
    current.current = null;
    localStorage.removeItem(ACCOUNT_CACHE_KEY);
    localStorage.removeItem(GUEST_BACKUP_KEY);
    restoreAccountProgress(guest);
    restoreRef.current();
    setAccount(null); setStatus('guest'); setError('');
  }

  async function resolveConflict(keepLocal) {
    if (flight.current) await flight.current.catch(() => {});
    const value = await communityRequest('/api/account/session');
    if (!value.account) throw new Error('Your session expired. Sign in again to sync.');
    localStorage.setItem('seamenese_conflict_backup_v1', JSON.stringify(captureAccountProgress()));
    if (!keepLocal) adopt(value);
    else {
      // This is an explicit choice to use this device's save. Normalize its
      // identity before uploading; automatic stale guest writes stay blocked.
      const player = JSON.parse(localStorage.getItem('seamenese_dopamine_v2') || '{}');
      localStorage.setItem('seamenese_dopamine_v2', JSON.stringify({ ...player, playerId: value.account.playerId }));
      restoreRef.current();
      current.current = { ...value, fingerprint: '', conflict: false };
      await sync();
    }
  }

  return { account, ready, sessionChecked, status, error, authenticate, logout, sync, resolveConflict };
}

function MigrationPrompt({ summary, onMigrate, onDismiss }) {
  const panel = React.useRef(null);
  React.useEffect(() => {
    const previousFocus = document.activeElement;
    panel.current?.querySelector('button[data-primary]')?.focus();
    const keys = event => {
      if (event.key === 'Escape') { event.preventDefault(); onDismiss(); }
      if (event.key === 'Tab') {
        const buttons = [...panel.current.querySelectorAll('button')];
        const first = buttons[0], last = buttons[buttons.length - 1];
        if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
        else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
      }
    };
    window.addEventListener('keydown', keys);
    return () => { window.removeEventListener('keydown', keys); previousFocus?.focus(); };
  }, []);
  return <div className="learn-modal-backdrop" onClick={onDismiss}>
    <section ref={panel} className="learn-modal-sheet migration-prompt" role="dialog" aria-modal="true" aria-labelledby="migration-title" aria-describedby="migration-description" onClick={e => e.stopPropagation()}>
      <div className="community-heading"><div><div className="eyebrow"><span className="rule" />Your saved progress</div><h2 id="migration-title" className="serif">Take your progress with you</h2></div><button className="btn" type="button" onClick={onDismiss} aria-label="Dismiss migration prompt">✕</button></div>
      <p id="migration-description">We found a learner profile saved in this browser. Create an account here to keep your progress and use it on other devices.</p>
      <ul className="migration-progress-list">
        <li><strong>{summary.points.toLocaleString()}</strong> {summary.points === 1 ? 'point' : 'points'}</li>
        <li><strong>{summary.solvedScenes}</strong> solved {summary.solvedScenes === 1 ? 'scene' : 'scenes'}</li>
        <li><strong>{summary.masteredWords}</strong> mastered {summary.masteredWords === 1 ? 'word' : 'words'}</li>
        <li><strong>{summary.srsCards}</strong> SRS {summary.srsCards === 1 ? 'card' : 'cards'}</li>
        {summary.cosmetics > 0 && <li><strong>{summary.cosmetics}</strong> unlocked cosmetics &amp; perks</li>}
      </ul>
      <p className="community-muted">Your progress is backed up before sign-up and linked when your account is created. Accounts are optional. You can also do this later from Account.</p>
      <div className="community-actions"><button type="button" className="btn btn-accent" data-primary onClick={onMigrate}>Create account &amp; keep progress</button><button type="button" className="btn" onClick={onDismiss}>Keep playing as guest</button></div>
    </section>
  </div>;
}

function AccountPanel({ open, onClose, sync, username, onOpenSettings, inline = false, initialMode = 'register' }) {
  const [mode, setMode] = React.useState(initialMode);
  const [name, setName] = React.useState(username || '');
  const [password, setPassword] = React.useState('');
  const [confirmation, setConfirmation] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const firstInput = React.useRef(null);
  const busyRef = React.useRef(busy);
  busyRef.current = busy;
  React.useEffect(() => {
    if (sync.status === 'signed out') { setMode('login'); setName(sync.account?.username || ''); }
  }, [sync.status]);
  React.useEffect(() => {
    if (!open) return;
    const previousFocus = document.activeElement;
    firstInput.current?.focus();
    const escape = event => {
      if (event.key === 'Escape' && !busyRef.current) onClose();
      if (event.key === 'Tab' && !inline) {
        const elements = [...document.querySelectorAll('.account-panel button:not([disabled]), .account-panel input:not([disabled])')];
        const first = elements[0], last = elements[elements.length - 1];
        if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
        else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
      }
    };
    window.addEventListener('keydown', escape);
    return () => { window.removeEventListener('keydown', escape); previousFocus?.focus(); };
  }, [open]);
  if (!open) return null;
  async function action(fn) {
    setBusy(true); setError('');
    try { await fn(); setPassword(''); setConfirmation(''); } catch (err) { setError(err.message); }
    finally { setBusy(false); }
  }
  const panel = (
      <section className={`account-panel ${inline ? 'account-inline' : 'learn-modal-sheet'}`} role={inline ? 'group' : 'dialog'} aria-modal={inline ? undefined : true} aria-labelledby="account-title" onClick={e => e.stopPropagation()}>
        <div className="community-heading"><h2 className="serif" id="account-title">{sync.account ? sync.account.username : mode === 'login' ? 'Sign in' : 'Create an account'}</h2><button className="btn" onClick={onClose} aria-label="Close account" disabled={busy}>✕</button></div>
        {sync.account && sync.status !== 'signed out' ? (
          <>
            <p>Progress sync: <strong role="status">{sync.status}</strong>. Your points, cosmetics, solved scenes and SRS deck travel with your account.</p>
            {sync.error && <p className="community-error" role="alert">{sync.error}</p>}
            {sync.status === 'conflict' ? <div className="community-actions">
              <button className="btn btn-accent" disabled={busy} onClick={() => action(() => sync.resolveConflict(false))}>Load account progress</button>
              <button className="btn" disabled={busy} onClick={() => action(() => sync.resolveConflict(true))}>Use this device’s progress</button>
              <p>A local backup is kept whichever you choose.</p>
            </div> : <button className="btn" disabled={busy} onClick={() => action(sync.sync)}>Sync now</button>}
            <p>Signing out returns to this device’s guest progress.</p>
            <button className="btn" disabled={busy || sync.status === 'conflict'} onClick={() => action(sync.logout)}>Sign out</button>
          </>
        ) : (
          <>
            <p>Accounts are optional. Keep learning as a guest, or save this device’s progress to use on another device.</p>
            <div className="community-actions">{!sync.account && <button className={`btn ${mode === 'register' ? 'btn-accent' : ''}`} onClick={() => { setMode('register'); setError(''); }}>Create account</button>}<button className={`btn ${mode === 'login' ? 'btn-accent' : ''}`} onClick={() => { setMode('login'); setError(''); }}>Sign in</button></div>
            <p className="community-muted">{mode === 'register' ? 'Create your account on the device that has your progress. We’ll link it automatically.' : 'Signing in loads your account progress. Your guest save stays on this device.'}</p>
            <form onSubmit={e => {
              e.preventDefault();
              if (!sync.account && mode === 'register') {
                const problem = newAccountPasswordError(password, confirmation);
                if (problem) { setError(problem); return; }
              }
              action(() => sync.authenticate(sync.account ? 'login' : mode, name, password));
            }}>
              <label className="community-field">Account username<input ref={firstInput} className="field" autoComplete="username" value={name} onChange={e => setName(e.target.value)} minLength={3} maxLength={mode === 'register' ? 20 : 28} pattern="[a-zA-Z0-9_.\-]+" required /></label>
              <label className="community-field">Password<input className="field" type="password" name="password" autoComplete={mode === 'register' ? 'new-password' : 'current-password'} value={password} onChange={e => setPassword(e.target.value)} maxLength={256} required /></label>
              {mode === 'register' && <label className="community-field">Confirm password<input className="field" type="password" name="password-confirmation" autoComplete="new-password" value={confirmation} onChange={e => setConfirmation(e.target.value)} maxLength={256} required /></label>}
              {mode === 'register' && <p className="community-muted">Use at least 15 characters—a short phrase works well—and keep your password somewhere safe. Your existing progress will be linked to this account.</p>}
              <button className="btn btn-accent" type="submit" disabled={busy || !sync.ready}>{busy ? 'Please wait…' : mode === 'register' ? 'Create account & save progress' : 'Sign in & load progress'}</button>
            </form>
          </>
        )}
        <div className="account-settings-link"><button className="btn" type="button" disabled={busy} onClick={() => { onClose(); onOpenSettings(); }}>Settings</button></div>
        {error && <p className="community-error" role="alert">{error}</p>}
        {!sync.account && sync.error && <p className="community-muted">{sync.error}</p>}
      </section>
  );
  return inline ? panel : <div className="learn-modal-backdrop" onClick={() => !busy && onClose()}>{panel}</div>;
}

function ChatEmbed({ url }) {
  const [expanded, setExpanded] = React.useState(false);
  const safe = window.SeameneseChatLanguage.safeUrl(url);
  if (!safe) return null;
  const parsed = new URL(safe);
  const image = /\.(gif|png|jpe?g|webp)$/i.test(parsed.pathname) || /^(media\d*\.giphy\.com|i\.giphy\.com|media\.tenor\.com)$/.test(parsed.hostname);
  const youtube = /^(www\.)?(youtube\.com|youtu\.be)$/.test(parsed.hostname);
  const videoId = youtube ? (parsed.hostname === 'youtu.be' ? parsed.pathname.slice(1) : parsed.searchParams.get('v') || parsed.pathname.match(/^\/shorts\/([^/]+)/)?.[1]) : null;
  return <div className="chat-embed">
    {image && <a href={safe} target="_blank" rel="noopener noreferrer"><img src={safe} alt="Shared image or GIF" loading="lazy" referrerPolicy="no-referrer" onError={e => { e.currentTarget.style.display = 'none'; }} /></a>}
    {videoId && /^[\w-]{11}$/.test(videoId) && (expanded ? <iframe title="Shared YouTube video" src={`https://www.youtube-nocookie.com/embed/${videoId}`} loading="lazy" allowFullScreen referrerPolicy="no-referrer" sandbox="allow-scripts allow-same-origin allow-presentation" /> : <button className="btn" onClick={() => setExpanded(true)}>▶ Load video</button>)}
    <a href={safe} target="_blank" rel="noopener noreferrer" className="chat-link"><strong>{parsed.hostname}</strong><span>{parsed.pathname.slice(0, 90)} ↗</span></a>
  </div>;
}

function ChatMessageText({ message }) {
  const mentions = new Map((message.mentions || []).map(m => [m.handle.toLowerCase(), m]));
  return message.text.split(/(@[\p{L}\p{N}_.-]+)/gu).map((part, i) => {
    const candidate = part.slice(1).toLowerCase();
    const handle = mentions.has(candidate) ? candidate : candidate.replace(/\.+$/, '');
    return part.startsWith('@') && mentions.has(handle) ? <React.Fragment key={i}><span className="chat-mention">{part.slice(0, handle.length + 1)}</span>{part.slice(handle.length + 1)}</React.Fragment> : <React.Fragment key={i}>{part}</React.Fragment>;
  });
}

const STANDARD_CHAT_REACTIONS = ['👍', '❤️', '😂', '🔥', '🐟', '🌊', '🎉', '✨', '🤝', '🍻', '🤔', '😮', '😢', '🙏', '💯'];

const SUPER_REACTION_CATALOG = [
  { id: 'react_super_wave', emoji: '🌊', name: 'Tidal Surge', animClass: 'super-anim-wave', cost: 450, particles: ['🌊', '🫧', '💦'] },
  { id: 'react_super_fish', emoji: '🐟', name: 'Golden Bonk Fish', animClass: 'super-anim-fish', cost: 500, particles: ['🐟', '✨', '🫧'] },
  { id: 'react_super_party', emoji: '🍻', name: 'Daka Tavern Toast', animClass: 'super-anim-party', cost: 550, particles: ['🍻', '🎉', '✨'] },
  { id: 'react_super_heart', emoji: '💘', name: 'Skantar Heartbeat', animClass: 'super-anim-heart', cost: 600, particles: ['💘', '💖', '✨'] },
  { id: 'react_super_fire', emoji: '🔥', name: 'Abyssal Hellfire', animClass: 'super-anim-fire', cost: 650, particles: ['🔥', '☄️', '✨'] },
  { id: 'react_super_crown', emoji: '👑', name: 'Sovereign Crown', animClass: 'super-anim-crown', cost: 800, particles: ['👑', '✨', '🪙'] },
  { id: 'react_super_gem', emoji: '💎', name: 'Trench Diamond', animClass: 'super-anim-gem', cost: 850, particles: ['💎', '💠', '✨'] },
  { id: 'react_super_bolt', emoji: '⚡', name: 'Overdrive Bolt', animClass: 'super-anim-bolt', cost: 950, particles: ['⚡', '💥', '✨'] },
  { id: 'react_super_octopus', emoji: '🐙', name: 'Kraken Awakening', animClass: 'super-anim-octopus', cost: 1100, particles: ['🐙', '🌊', '🫧'] },
  { id: 'react_super_galaxy', emoji: '🌌', name: 'Cosmic Singularity', animClass: 'super-anim-galaxy', cost: 1200, particles: ['🌌', '🪐', '✦'] },
  { id: 'react_super_rocket', emoji: '🚀', name: 'Starlight Rocket', animClass: 'super-anim-rocket', cost: 1350, particles: ['🚀', '☄️', '🌟'] },
  { id: 'react_super_dragon', emoji: '🐉', name: 'Leviathan Roar', animClass: 'super-anim-dragon', cost: 1600, particles: ['🐉', '🔥', '⚡'] },
];
window.SUPER_REACTION_CATALOG = SUPER_REACTION_CATALOG;

function SuperReactionPill({
  messageId,
  reaction,
  count,
  mine,
  superItem,
  animClass,
  burstNonce,
  seenSetRef,
  scrollRootRef,
  onClick,
  title,
}) {
  const pillRef = React.useRef(null);
  const [spawning, setSpawning] = React.useState(false);
  const timerRef = React.useRef(null);
  const seenKey = `${messageId}:${reaction.emoji}`;
  const particles = superItem?.particles || ['✨', '✦', reaction.emoji];

  const triggerSpawn = React.useCallback(() => {
    setSpawning(false);
    requestAnimationFrame(() => {
      setSpawning(true);
      if (timerRef.current) clearTimeout(timerRef.current);
      timerRef.current = setTimeout(() => setSpawning(false), 1250);
    });
  }, []);

  React.useEffect(() => {
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, []);

  React.useEffect(() => {
    if (!burstNonce) return;
    seenSetRef.current.add(seenKey);
    triggerSpawn();
  }, [burstNonce, seenKey, seenSetRef, triggerSpawn]);

  React.useEffect(() => {
    const node = pillRef.current;
    if (!node || seenSetRef.current.has(seenKey)) return;
    if (typeof IntersectionObserver === 'undefined') {
      seenSetRef.current.add(seenKey);
      triggerSpawn();
      return;
    }
    const observer = new IntersectionObserver(
      entries => {
        for (const entry of entries) {
          if (entry.isIntersecting && !seenSetRef.current.has(seenKey)) {
            seenSetRef.current.add(seenKey);
            triggerSpawn();
            observer.disconnect();
            break;
          }
        }
      },
      { root: scrollRootRef?.current || null, threshold: 0.15 }
    );
    observer.observe(node);
    return () => observer.disconnect();
  }, [seenKey, seenSetRef, scrollRootRef, triggerSpawn]);

  return (
    <button
      ref={pillRef}
      type="button"
      className={`chat-reaction-pill is-super ${animClass} ${mine ? 'is-mine' : ''} ${spawning ? 'is-spawning' : ''}`}
      onClick={onClick}
      title={title}
    >
      {spawning && (
        <span className="chat-super-spawn-particles" aria-hidden="true">
          {particles.map((p, idx) => (
            <span key={idx} className={`chat-super-particle p-${idx}`}>{p}</span>
          ))}
        </span>
      )}
      <span className="chat-reaction-emoji">{reaction.emoji}</span>
      <span className="chat-reaction-count mono">{count}</span>
      <span className="chat-super-sparkle" aria-hidden="true">✦</span>
    </button>
  );
}

function ChatView({ dopamine, account, onOpenAccount, onOpenShop, onSpendPoints, isAdmin }) {
  const [messages, setMessages] = React.useState([]);
  const [words, setWords] = React.useState([]);
  const [users, setUsers] = React.useState([]);
  const [viewerIdentities, setViewerIdentities] = React.useState([]);
  const [superQuota, setSuperQuota] = React.useState({ used: 0, remaining_free: 5, free_limit: 5, extra_cost: 800 });
  const [mentionQuery, setMentionQuery] = React.useState(null);
  const [mentionIndex, setMentionIndex] = React.useState(0);
  const [text, setText] = React.useState('');
  const [replyingTo, setReplyingTo] = React.useState(null);
  const [editingId, setEditingId] = React.useState(null);
  const [editText, setEditText] = React.useState('');
  const [editConfirmText, setEditConfirmText] = React.useState(null);
  const [reactionPickerMsgId, setReactionPickerMsgId] = React.useState(null);
  const [superBursts, setSuperBursts] = React.useState({});
  const [highlightedMsgId, setHighlightedMsgId] = React.useState(null);
  const [error, setError] = React.useState('');
  const [connection, setConnection] = React.useState('connecting');
  const [busy, setBusy] = React.useState(false);
  const [emojiOpen, setEmojiOpen] = React.useState(false);
  const [mediaOpen, setMediaOpen] = React.useState(false);
  const [mediaUrl, setMediaUrl] = React.useState('');
  const [confirmText, setConfirmText] = React.useState(null);
  const scroll = React.useRef(null);
  const follow = React.useRef(true);
  const input = React.useRef(null);
  const deleted = React.useRef(new Set());
  const arrivals = React.useRef(new Map());
  const arrivalCounter = React.useRef(0);
  const seenSuperReactionsRef = React.useRef(new Set());
  const check = React.useMemo(() => window.SeameneseChatLanguage.check(text, words), [text, words]);
  const editCheck = React.useMemo(() => window.SeameneseChatLanguage.check(editText, words), [editText, words]);

  const viewerIdSet = React.useMemo(() => {
    return new Set([
      ...viewerIdentities,
      account?.playerId,
      ...(account?.legacyPlayerIds || []),
      dopamine?.playerId,
    ].filter(Boolean));
  }, [viewerIdentities, account, dopamine?.playerId]);

  const unlockedSet = React.useMemo(() => new Set(dopamine?.unlockedItems || []), [dopamine?.unlockedItems]);

  function merge(list) {
    for (const message of list) arrivals.current.set(message.id, ++arrivalCounter.current);
    setMessages(previous => [...new Map([...previous, ...list].map(m => [m.id, m])).values()].filter(m => !deleted.current.has(m.id)).sort((a, b) => a.created_at.localeCompare(b.created_at)).slice(-150));
  }

  React.useEffect(() => {
    let active = true;
    let refreshCount = 0;
    async function refresh() {
      const requestId = ++refreshCount;
      const before = arrivalCounter.current;
      try {
        const result = await communityRequest('/api/chat');
        if (active && requestId === refreshCount) {
          // The snapshot also removes messages moderated while disconnected.
          // Preserve any live arrivals delivered after this request began.
          setMessages(previous => [...new Map([...result.messages, ...previous.filter(m => (arrivals.current.get(m.id) || 0) > before)].map(m => [m.id, m])).values()].filter(m => !deleted.current.has(m.id)).sort((a, b) => a.created_at.localeCompare(b.created_at)).slice(-150));
          setWords(result.vocabulary); setError('');
          setUsers(result.users || []);
          if (Array.isArray(result.viewer_identities)) {
            setViewerIdentities(prev => [...new Set([...prev, ...result.viewer_identities, result.viewer_id].filter(Boolean))]);
          }
          if (result.super_reaction_quota) {
            setSuperQuota(result.super_reaction_quota);
          }
        }
      } catch (err) { if (active) setError(err.message); }
    }
    refresh();
    const events = new EventSource('/api/events');
    events.onopen = () => { if (active) { setConnection('live'); refresh(); } };
    events.onerror = () => { if (active) setConnection('reconnecting'); };
    events.addEventListener('chat-message', e => { try { merge([JSON.parse(e.data)]); } catch {} });
    events.addEventListener('chat-edited', e => { try { merge([JSON.parse(e.data)]); } catch {} });
    events.addEventListener('chat-reaction', e => {
      try {
        const payload = JSON.parse(e.data);
        if (payload?.message) merge([payload.message]);
      } catch {}
    });
    events.addEventListener('chat-deleted', e => { try { const { id } = JSON.parse(e.data); deleted.current.add(id); setMessages(prev => prev.filter(m => m.id !== id)); } catch {} });
    events.addEventListener('dictionary-updated', refresh);
    events.addEventListener('chat-users-updated', async () => {
      try { const result = await communityRequest('/api/chat/users'); if (active) setUsers(result.users || []); } catch {}
    });
    return () => { active = false; events.close(); };
  }, []);

  React.useEffect(() => { if (follow.current && scroll.current) scroll.current.scrollTop = scroll.current.scrollHeight; }, [messages.length]);

  function insert(value) {
    const element = input.current;
    const start = element?.selectionStart ?? text.length;
    const end = element?.selectionEnd ?? start;
    setText((text.slice(0, start) + value + text.slice(end)).slice(0, 2000));
    setConfirmText(null);
    requestAnimationFrame(() => { element?.focus(); element?.setSelectionRange(start + value.length, start + value.length); });
  }

  async function send(allowUnknown = false) {
    if (!text.trim() || busy) return;
    if (!check.ok && !allowUnknown) { setConfirmText(text); return; }
    setBusy(true); setError('');
    try {
      const result = await communityRequest('/api/chat', {
        method: 'POST',
        body: JSON.stringify({
          text,
          allow_unknown: allowUnknown,
          profile: dopamine,
          ...(replyingTo ? { reply_to: replyingTo.id } : {}),
        }),
      });
      if (Array.isArray(result.viewer_identities)) {
        setViewerIdentities(prev => [...new Set([...prev, ...result.viewer_identities, result.viewer_id].filter(Boolean))]);
      }
      follow.current = true;
      merge([result.message]);
      setText('');
      setReplyingTo(null);
      setConfirmText(null);
      setMentionQuery(null);
      input.current?.focus();
    } catch (err) { setError(err.message); if (err.status === 422) setConfirmText(text); }
    finally { setBusy(false); }
  }

  function startEdit(message) {
    setEditingId(message.id);
    setEditText(message.text || '');
    setEditConfirmText(null);
    setError('');
  }

  function cancelEdit() {
    setEditingId(null);
    setEditText('');
    setEditConfirmText(null);
  }

  async function saveEdit(messageId, allowUnknown = false) {
    if (!editText.trim() || busy) return;
    if (!editCheck.ok && !allowUnknown) {
      setEditConfirmText(editText);
      return;
    }
    setBusy(true); setError('');
    try {
      const result = await communityRequest(`/api/chat/${encodeURIComponent(messageId)}`, {
        method: 'PATCH',
        body: JSON.stringify({ text: editText, allow_unknown: allowUnknown }),
      });
      if (result.message) merge([result.message]);
      cancelEdit();
    } catch (err) {
      setError(err.message);
      if (err.status === 422) setEditConfirmText(editText);
    } finally {
      setBusy(false);
    }
  }

  async function remove(id, isOwn = false) {
    try {
      await communityRequest(`/api/chat/${encodeURIComponent(id)}`, {
        method: 'DELETE',
        headers: isOwn && !isAdmin ? {} : adminHeaders(),
        body: '{}',
      });
      deleted.current.add(id);
      setMessages(prev => prev.filter(m => m.id !== id));
      if (replyingTo?.id === id) setReplyingTo(null);
      if (editingId === id) cancelEdit();
    } catch (err) { setError(err.message); }
  }

  function triggerSuperBurst(messageId, emoji) {
    const burstKey = `${messageId}:${emoji}`;
    setSuperBursts(prev => ({ ...prev, [burstKey]: (prev[burstKey] || 0) + 1 }));
    if (window.playDopamineSynth) {
      window.playDopamineSynth('unlock', dopamine?.soundEnabled, dopamine?.equippedSound);
    }
  }

  async function toggleReaction(messageId, emoji, animatedItem = null) {
    setError('');
    try {
      const result = await communityRequest(`/api/chat/${encodeURIComponent(messageId)}/reactions`, {
        method: 'POST',
        body: JSON.stringify({
          emoji,
          ...(animatedItem ? {
            animated_id: animatedItem.id,
            unlocked_items: dopamine?.unlockedItems || [],
            points: dopamine?.points || 0,
          } : {}),
        }),
      });
      if (Array.isArray(result.viewer_identities)) {
        setViewerIdentities(prev => [...new Set([...prev, ...result.viewer_identities, result.viewer_id].filter(Boolean))]);
      }
      if (result.super_reaction_quota) {
        setSuperQuota(result.super_reaction_quota);
      }
      if (Number(result.charged_points) > 0 && typeof onSpendPoints === 'function') {
        onSpendPoints(Number(result.charged_points));
      }
      if (result.message) merge([result.message]);
      if (animatedItem) {
        triggerSuperBurst(messageId, animatedItem.emoji);
      }
      setReactionPickerMsgId(null);
    } catch (err) {
      setError(err.message);
    }
  }

  function jumpToMessage(targetId) {
    if (!targetId) return;
    const el = document.getElementById(`chat-message-${targetId}`);
    if (el) {
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      setHighlightedMsgId(targetId);
      setTimeout(() => setHighlightedMsgId(prev => (prev === targetId ? null : prev)), 1800);
    }
  }

  const groups = React.useMemo(() => SeameneseCommunityModel.groupMessages(messages), [messages]);
  const mentionUsers = mentionQuery ? users.filter(u => u.id !== dopamine.playerId && `${u.username} ${u.handle}`.toLowerCase().includes(mentionQuery.query.toLowerCase())).slice(0, 8) : [];
  function updateMention(value, caret) {
    const match = value.slice(0, caret).match(/(?:^|\s)@([\p{L}\p{N}_.-]*)$/u);
    setMentionQuery(match ? { query: match[1], from: caret - match[1].length - 1, to: caret } : null);
    setMentionIndex(0);
  }
  function chooseMention(user) {
    if (!mentionQuery) return;
    const mention = `@${user.handle} `;
    const caret = mentionQuery.from + mention.length;
    setText((text.slice(0, mentionQuery.from) + mention + text.slice(mentionQuery.to)).slice(0, 2000));
    setMentionQuery(null); setConfirmText(null);
    requestAnimationFrame(() => { input.current?.focus(); input.current?.setSelectionRange(caret, caret); });
  }
  return (
    <section className="chat-room panel">
      <div className="community-heading"><div><div className="eyebrow"><span className="rule" />Community · {connection}</div><h2 className="serif page-title">The global chat room</h2></div></div>
      <p className="community-muted">Obisëi Seamenese, ta buanon! Chat together, share GIFs, and try new words. Guests are welcome.</p>
      <div className="chat-history" ref={scroll} role="log" aria-label="Global chat messages" aria-live="polite" onScroll={() => { const e = scroll.current; follow.current = e.scrollHeight - e.scrollTop - e.clientHeight < 80; }}>
        {!messages.length && <p className="chat-empty">{connection === 'connecting' ? 'Loading the conversation…' : 'Tamako! Start the conversation.'}</p>}
        {groups.map(group => {
          const np = getEquippedNameplateObj(group.equipped_nameplate);
          const userTitle = getEquippedTitleObj(group.equipped_title);
          return <article className="chat-message-group" key={group.id}>
            <div className="chat-message-meta"><strong className={`seamenese-nameplate ${np.cssClass}`}>{group.username}</strong><span className="chat-title">{userTitle.name}</span>{group.guest && <span className="chat-guest">guest</span>}<time dateTime={group.created_at} title={new Date(group.created_at).toLocaleString()}>{new Date(group.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</time></div>
            {group.messages.map(message => {
              const isOwn = Boolean(message.author_id && viewerIdSet.has(message.author_id));
              const canDelete = isOwn || Boolean(isAdmin);
              const isEditing = editingId === message.id;
              const reactions = Array.isArray(message.reactions) ? message.reactions : [];
              const pickerOpen = reactionPickerMsgId === message.id;

              return <div className={`chat-message-entry ${highlightedMsgId === message.id ? 'is-highlighted' : ''}`} key={message.id} id={`chat-message-${message.id}`}>
                {message.reply_to && (
                  <button
                    type="button"
                    className="chat-reply-quote"
                    onClick={() => jumpToMessage(message.reply_to.id)}
                    title="Jump to replied message"
                  >
                    <span className="chat-reply-icon" aria-hidden="true">↩</span>
                    <strong className="chat-reply-author">@{message.reply_to.username}</strong>
                    <span className="chat-reply-snippet">{message.reply_to.text}</span>
                  </button>
                )}
                <div className="chat-entry-actions">
                  <time dateTime={message.created_at}>{new Date(message.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</time>
                  {!message.language_checked && <span title="Contains words outside the current dictionary">◇</span>}
                  <button
                    type="button"
                    className="chat-action-btn"
                    onClick={() => {
                      setReplyingTo({ id: message.id, username: message.username, text: message.text });
                      input.current?.focus();
                    }}
                    title={`Reply to ${message.username}`}
                    aria-label={`Reply to ${message.username}`}
                  >
                    ↩
                  </button>
                  <button
                    type="button"
                    className={`chat-action-btn ${pickerOpen ? 'is-active' : ''}`}
                    onClick={() => setReactionPickerMsgId(pickerOpen ? null : message.id)}
                    title="Add reaction"
                    aria-label={`React to message by ${message.username}`}
                    aria-expanded={pickerOpen}
                  >
                    ☺+
                  </button>
                  {isOwn && !isEditing && (
                    <button
                      type="button"
                      className="chat-action-btn"
                      onClick={() => startEdit(message)}
                      title="Edit your message"
                      aria-label="Edit message"
                    >
                      ✎
                    </button>
                  )}
                  {canDelete && (
                    <button
                      type="button"
                      className="chat-delete"
                      onClick={() => remove(message.id, isOwn)}
                      title={isOwn ? 'Delete your message' : `Delete message by ${message.username}`}
                      aria-label={`Delete message by ${message.username}`}
                    >
                      ✕
                    </button>
                  )}
                </div>

                {isEditing ? (
                  <div className="chat-edit-box">
                    <textarea
                      className="field field-mono chat-edit-textarea"
                      value={editText}
                      maxLength={2000}
                      rows={2}
                      onChange={e => { setEditText(e.target.value); setEditConfirmText(null); }}
                      onKeyDown={e => {
                        if (e.key === 'Escape') { e.preventDefault(); cancelEdit(); }
                        if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
                          e.preventDefault();
                          saveEdit(message.id, false);
                        }
                      }}
                    />
                    {editConfirmText === editText && (
                      <div className="chat-confirm">
                        <p>Contains unfamiliar words ({editCheck.unknown.slice(0, 6).join(', ')}).</p>
                        <button type="button" className="btn btn-warm" disabled={busy} onClick={() => saveEdit(message.id, true)}>Save with unfamiliar words</button>
                      </div>
                    )}
                    <div className="chat-edit-actions">
                      <button type="button" className="btn btn-accent" disabled={busy || !editText.trim()} onClick={() => saveEdit(message.id, false)}>Save</button>
                      <button type="button" className="btn" disabled={busy} onClick={cancelEdit}>Cancel</button>
                    </div>
                  </div>
                ) : (
                  <p className="chat-message-text">
                    <ChatMessageText message={message} />
                    {message.edited_at && (
                      <span className="chat-edited-tag" title={`Edited ${new Date(message.edited_at).toLocaleString()}`}> (edited)</span>
                    )}
                  </p>
                )}

                {message.links?.length > 0 && <div className="chat-embeds">{message.links.map(url => <ChatEmbed key={url} url={url} />)}</div>}

                {pickerOpen && (
                  <div className="chat-reaction-popover" role="dialog" aria-label="Choose a reaction">
                    <div className="chat-reaction-popover-section">
                      <div className="chat-reaction-popover-head">
                        <span>⚡ Super Reactions</span>
                        {onOpenShop && (
                          <button type="button" className="chat-reaction-shop-link" onClick={() => { setReactionPickerMsgId(null); onOpenShop(); }}>
                            🛒 Unlock more in Store
                          </button>
                        )}
                      </div>
                      <div className="chat-super-quota-banner" role="status">
                        {superQuota.remaining_free > 0 ? (
                          <span>🎁 <strong>{superQuota.remaining_free}/{superQuota.free_limit}</strong> free today · Resets at 00:00 UTC</span>
                        ) : (
                          <span>🪙 Daily free limit reached · Extra Super Reactions cost <strong>{superQuota.extra_cost} coins</strong> (resets 00:00 UTC)</span>
                        )}
                      </div>
                      <div className="chat-super-reaction-grid">
                        {SUPER_REACTION_CATALOG.map(sr => {
                          const unlocked = unlockedSet.has(sr.id);
                          const extraCostTag = unlocked && superQuota.remaining_free <= 0 ? ` · 🪙 ${superQuota.extra_cost}p` : '';
                          return (
                            <button
                              key={sr.id}
                              type="button"
                              className={`chat-super-reaction-option ${sr.animClass} ${unlocked ? 'is-unlocked' : 'is-locked'}`}
                              onClick={() => {
                                if (unlocked) toggleReaction(message.id, sr.emoji, sr);
                                else if (onOpenShop) { setReactionPickerMsgId(null); onOpenShop(); }
                              }}
                              title={unlocked ? `${sr.name} (Super Reaction${extraCostTag})` : `${sr.name} — Unlock in Store (${sr.cost}p)`}
                            >
                              <span className="chat-super-emoji">{sr.emoji}</span>
                              <span className="chat-super-label">{unlocked ? sr.name : `🔒 ${sr.cost}p`}</span>
                            </button>
                          );
                        })}
                      </div>
                    </div>
                    <div className="chat-reaction-popover-section">
                      <div className="chat-reaction-popover-head">
                        <span>Standard Emojis</span>
                        <span className="mono">{reactions.length}/15 unique</span>
                      </div>
                      <div className="chat-standard-reaction-grid">
                        {STANDARD_CHAT_REACTIONS.map(emoji => {
                          const existing = reactions.find(r => r.emoji === emoji);
                          const disabled = !existing && reactions.length >= 15;
                          return (
                            <button
                              key={emoji}
                              type="button"
                              className="chat-reaction-option"
                              disabled={disabled}
                              onClick={() => toggleReaction(message.id, emoji, null)}
                              title={disabled ? 'Maximum 15 unique emojis reached' : `React with ${emoji}`}
                            >
                              {emoji}
                            </button>
                          );
                        })}
                      </div>
                    </div>
                  </div>
                )}

                {reactions.length > 0 && (
                  <div className="chat-reactions-bar" aria-label="Message reactions">
                    {reactions.map(r => {
                      const count = Array.isArray(r.users) ? r.users.length : 0;
                      if (count <= 0) return null;
                      const mine = Array.isArray(r.users) && r.users.some(uid => viewerIdSet.has(uid));
                      const isSuper = (Array.isArray(r.super_users) && r.super_users.length > 0) || (Array.isArray(r.animated_ids) && r.animated_ids.length > 0);
                      const firstAnimId = Array.isArray(r.animated_ids) && r.animated_ids[0];
                      const superItem = SUPER_REACTION_CATALOG.find(item => item.id === firstAnimId || item.emoji === r.emoji);
                      const animClass = isSuper ? (superItem?.animClass || 'super-anim-wave') : '';
                      const burstNonce = superBursts[`${message.id}:${r.emoji}`] || 0;

                      if (isSuper) {
                        return (
                          <SuperReactionPill
                            key={r.emoji}
                            messageId={message.id}
                            reaction={r}
                            count={count}
                            mine={mine}
                            superItem={superItem}
                            animClass={animClass}
                            burstNonce={burstNonce}
                            seenSetRef={seenSuperReactionsRef}
                            scrollRootRef={scroll}
                            onClick={() => {
                              const unlockedSuper = superItem && unlockedSet.has(superItem.id) ? superItem : null;
                              toggleReaction(message.id, r.emoji, unlockedSuper);
                            }}
                            title={`Super Reaction ${r.emoji} (${count})`}
                          />
                        );
                      }

                      return (
                        <button
                          key={r.emoji}
                          type="button"
                          className={`chat-reaction-pill ${mine ? 'is-mine' : ''}`}
                          onClick={() => toggleReaction(message.id, r.emoji, null)}
                          title={`React with ${r.emoji} (${count})`}
                        >
                          <span className="chat-reaction-emoji">{r.emoji}</span>
                          <span className="chat-reaction-count mono">{count}</span>
                        </button>
                      );
                    })}
                    {reactions.length < 15 && (
                      <button
                        type="button"
                        className="chat-reaction-add-pill"
                        onClick={() => setReactionPickerMsgId(pickerOpen ? null : message.id)}
                        title="Add reaction"
                        aria-label="Add reaction"
                      >
                        +
                      </button>
                    )}
                  </div>
                )}
              </div>;
            })}
          </article>;
        })}
      </div>
      <form className="chat-composer" onSubmit={e => { e.preventDefault(); send(); }}>
        {replyingTo && (
          <div className="chat-replying-banner" role="status">
            <div className="chat-replying-info">
              <span className="chat-reply-icon" aria-hidden="true">↩</span>
              <span>Replying to <strong>@{replyingTo.username}</strong>:</span>
              <span className="chat-replying-preview">{String(replyingTo.text || '').slice(0, 90)}</span>
            </div>
            <button type="button" className="btn chat-replying-cancel" onClick={() => setReplyingTo(null)} aria-label="Cancel reply">✕</button>
          </div>
        )}
        <label className="community-field">Your message<textarea className="field field-mono" ref={input} value={text} maxLength={2000} rows={3} placeholder="Tamako, ta buanon! Type @ to mention someone." aria-controls={mentionUsers.length ? 'chat-mention-options' : undefined} aria-expanded={mentionUsers.length > 0} onClick={e => updateMention(e.target.value, e.target.selectionStart)} onChange={e => { setText(e.target.value); setConfirmText(null); updateMention(e.target.value, e.target.selectionStart); }} onKeyDown={e => {
          if (mentionUsers.length && ['ArrowDown', 'ArrowUp', 'Tab', 'Enter', 'Escape'].includes(e.key)) {
            e.preventDefault();
            if (e.key === 'Escape') setMentionQuery(null);
            else if (e.key === 'ArrowDown') setMentionIndex(i => (i + 1) % mentionUsers.length);
            else if (e.key === 'ArrowUp') setMentionIndex(i => (i + mentionUsers.length - 1) % mentionUsers.length);
            else chooseMention(mentionUsers[mentionIndex % mentionUsers.length]);
            return;
          }
          if (e.key === 'Escape' && replyingTo) { e.preventDefault(); setReplyingTo(null); return; }
          if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); send(); }
        }} /></label>
        {mentionUsers.length > 0 && <div className="chat-mention-options" id="chat-mention-options" role="listbox" aria-label="Mention a user">{mentionUsers.map((user, i) => <button key={user.id} type="button" role="option" aria-selected={mentionIndex === i} onMouseDown={e => e.preventDefault()} onClick={() => chooseMention(user)}><strong>{user.username}</strong><span>@{user.handle}{user.guest ? ' · guest' : ''}</span></button>)}</div>}
        <div className="community-actions"><button className="btn" type="button" aria-expanded={emojiOpen} onClick={() => setEmojiOpen(!emojiOpen)}>☺ Emoji</button><button className="btn" type="button" aria-expanded={mediaOpen} onClick={() => setMediaOpen(!mediaOpen)}>GIF / link</button><span className="chat-count mono">{text.length}/2000</span><button className="btn btn-accent" disabled={busy || !text.trim()} type="submit">{busy ? 'Sending…' : 'Send ↵'}</button></div>
        {emojiOpen && <div className="chat-emoji-picker">{['🌊', '🐟', '🤝', '😂', '❤️', '🔥', '✨', '🎉', '🍻', '🤔', '👍', '👋'].map(emoji => <button className="btn" type="button" key={emoji} onClick={() => insert(emoji)}>{emoji}</button>)}</div>}
        {mediaOpen && <div className="community-actions"><label className="community-field">HTTPS image, GIF, video or website link<input type="url" className="field" value={mediaUrl} onChange={e => setMediaUrl(e.target.value)} placeholder="https://…" /></label><button type="button" className="btn" onClick={() => { const safe = window.SeameneseChatLanguage.safeUrl(mediaUrl); if (safe) { insert(` ${safe} `); setMediaUrl(''); setMediaOpen(false); } else setError('Use a public HTTPS link.'); }}>Add link</button></div>}
        {text.trim() && <p className={`chat-check ${check.ok ? 'is-ok' : ''}`} role="status">{check.ok ? '✓ Words match the current Seamenese dictionary.' : `◇ Unfamiliar words: ${check.unknown.slice(0, 12).join(', ')}. Names and new words may need a second look.`} <span className="community-muted">Dictionary check; grammar is not verified.</span></p>}
        {confirmText === text && <div className="chat-confirm"><p>Check the unfamiliar words above. You can still send names, borrowings and newly coined words.</p><button type="button" className="btn btn-warm" disabled={busy} onClick={() => send(true)}>Send with unfamiliar words</button></div>}
        {error && <p className="community-error" role="alert">{error}</p>}
      </form>
    </section>
  );
}
