(function () {
  const boot = window.UGFX_CHAT_BOOT || null;
  const app = document.getElementById('ugfxChatApp');
  if (!app || !boot) return;

  const feed = app.querySelector('[data-chat-feed]');
  const form = app.querySelector('[data-chat-form]');
  const attachments = app.querySelector('[data-chat-attachments]');
  const statusEl = app.querySelector('[data-chat-status]');
  const recordStatus = app.querySelector('[data-record-status]');
  const countEl = app.querySelector('[data-chat-count]');
  const nameInput = form.querySelector('[name="name"]');
  const messageInput = form.querySelector('[name="message"]');
  const gifInput = form.querySelector('[name="gif_url"]');
  const stickerInput = form.querySelector('[name="sticker"]');
  const voiceInput = form.querySelector('[name="voice_url"]');
  const voiceMimeInput = form.querySelector('[name="voice_mime"]');
  const panels = [...app.querySelectorAll('[data-panel]')];
  const panelButtons = [...app.querySelectorAll('[data-panel-toggle]')];
  const emojiGrid = app.querySelector('[data-emoji-grid]');
  const emojiSearch = app.querySelector('[data-emoji-search]');
  const emojiFilters = [...app.querySelectorAll('[data-emoji-filter]')];
  const stickerGrid = app.querySelector('[data-sticker-grid]');
  const gifUrlInput = app.querySelector('[data-gif-url-input]');
  const gifAttach = app.querySelector('[data-gif-attach]');
  const gifPresets = app.querySelector('[data-gif-presets]');
  const colorPicker = app.querySelector('[data-text-color-picker]');
  const recordButton = app.querySelector('[data-record-toggle]');
  const getUrl = app.dataset.chatGet;
  const postUrl = app.dataset.chatPost;
  const uploadUrl = app.dataset.chatUpload;
  const assetPrefix = app.dataset.assetPrefix || '';
  const adminName = boot.adminName || app.dataset.adminName || '';
  const adminRole = boot.adminRole || app.dataset.adminRole || 'admin';
  const postingBadge = app.querySelector('[data-posting-badge]');
  const textColorInput = form.querySelector('[name="text_color"]');
  const mediaManifestUrl = app.dataset.mediaManifest || (assetPrefix + 'assets/chat-media/manifest.json');
  const stickerSearch = app.querySelector('[data-sticker-search]');
  const gifSearch = app.querySelector('[data-gif-search]');
  const stickerFiltersHost = app.querySelector('[data-sticker-filters]');
  const gifFiltersHost = app.querySelector('[data-gif-filters]');
  const stickerLoadMore = app.querySelector('[data-sticker-load-more]');
  const gifLoadMore = app.querySelector('[data-gif-load-more]');
  const stickerCount = app.querySelector('[data-sticker-count]');
  const gifCount = app.querySelector('[data-gif-count]');

  const EMOJIS = [
    ['😀','faces','grin smile happy'],['😃','faces','smile happy bright'],['😄','faces','grin happy laugh'],['😁','faces','teeth smile'],['😆','faces','laugh lol'],['😅','faces','sweat laugh'],['😂','faces','cry laugh lol'],['🤣','faces','rofl lol'],['😊','faces','blush smile'],['🙂','faces','smile soft'],['😉','faces','wink'],['😌','faces','calm'],['😍','love','heart eyes love'],['🥰','love','hearts adore'],['😘','love','kiss'],['😎','faces','cool shades'],['🤩','faces','star eyes'],['🥳','party','party celebrate'],['😇','faces','angel'],['🤗','love','hug'],['🫶','love','heart hands'],['🤘','energy','rock hand'],['🔥','energy','fire hot'],['⚡','energy','electric power'],['🚀','energy','rocket launch'],['💥','energy','boom'],['🎧','music','headphones dj'],['🎤','music','microphone sing'],['🎶','music','notes music'],['🎵','music','note'],['📻','music','radio'],['🪩','party','disco dance'],['💃','party','dance'],['🕺','party','dance'],['🙌','party','celebrate hands'],['👏','party','clap'],['❤️','love','heart red'],['🧡','love','orange heart'],['💛','love','yellow heart'],['💚','love','green heart'],['💙','love','blue heart'],['💜','love','purple heart'],['🖤','love','black heart'],['🤍','love','white heart'],['🤎','love','brown heart'],['💯','energy','hundred'],['🎉','party','confetti'],['🎊','party','celebration'],['🥂','party','cheers'],['🍾','party','bottle'],['🌈','party','rainbow'],['🌟','energy','star'],['✨','energy','sparkles'],['🎚️','music','fader mixer'],['🎛️','music','control mixer'],['🔊','music','loud volume'],['🔈','music','speaker'],['📣','energy','megaphone'],['📀','music','disc'],['💿','music','cd'],['🫡','faces','respect salute'],['🤝','love','respect handshake'],['🙏','love','thanks please'],['😮‍💨','faces','relief'],['😤','faces','pumped'],['😈','energy','devil'],['👑','party','crown'],['☄️','energy','comet'],['🌙','party','night'],['☀️','party','sun'],['🌌','party','galaxy'],['🫶🏻','love','heart hands light'],['🫶🏽','love','heart hands medium'],['🫶🏿','love','heart hands dark'],['🤟','energy','love you hand'],['🤞','energy','fingers crossed'],['👌','faces','ok'],['👍','energy','thumbs up'],['👊','energy','fist'],['🙃','faces','silly'],['😜','faces','wink tongue'],['🤯','faces','mind blown'],['🥵','faces','hot'],['🥶','faces','cold'],['👀','faces','eyes'],['💫','energy','dizzy star'],['🎸','music','guitar'],['🥁','music','drums'],['🎹','music','piano'],['🎷','music','sax'],['🎺','music','trumpet'],['🪘','music','drum'],['🎻','music','violin'],['📺','party','watch'],['🍿','party','snack'],['☕','party','coffee'],['🍻','party','beer cheers'],['🧃','party','drink'],['🛸','energy','ufo'],['🪄','energy','magic wand'],['🎯','energy','target'],['🧨','energy','explosive'],['🌊','party','wave'],['🌴','party','summer'],['🌠','party','shooting star'],['😻','love','cat heart eyes'],['🤪','faces','zany'],['🤠','faces','cowboy'],['😏','faces','smirk'],['😴','faces','sleep'],['🥹','love','happy tears'],['🫠','faces','melting'],['🎼','music','score'],['🎬','party','showtime'],['🎙️','music','studio mic'],['🛰️','music','broadcast'],['🫨','energy','shaking'],['🎇','party','fireworks'],['🎆','party','fireworks'],['🩵','love','light blue heart'],['🏆','party','winner'],['🦄','party','magic'],['😺','faces','cat smile']
  ];

  let messages = Array.isArray(boot.initialMessages) ? boot.initialMessages.slice() : [];
  let currentEmojiFilter = 'all';
  let mediaRecorder = null;
  let recorderChunks = [];
  let recording = false;
  let selectedVoiceBlob = null;
  let mediaPack = { stickers: [], gifs: [], sticker_categories: [{ id: 'all', label: 'All' }], gif_categories: [{ id: 'all', label: 'All' }] };
  let stickerState = { filter: 'all', search: '', limit: 24 };
  let gifState = { filter: 'all', search: '', limit: 16 };

  function escapeHtml(text) {
    return String(text || '').replace(/[&<>'"]/g, s => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#039;','"':'&quot;'}[s]));
  }
  function autolink(text) {
    return escapeHtml(text).replace(/(https?:\/\/[^\s<]+)/gi, '<a href="$1" target="_blank" rel="noopener noreferrer nofollow">$1</a>').replace(/\n/g, '<br>');
  }
  function formatTime(ts) {
    if (!ts) return '';
    const d = new Date(String(ts).replace(' ', 'T'));
    if (Number.isNaN(d.getTime())) return ts;
    return d.toLocaleString([], { hour: 'numeric', minute: '2-digit', month: 'short', day: 'numeric' });
  }
  function initialFor(name) {
    return (name || 'G').trim().charAt(0).toUpperCase() || 'G';
  }
  function mediaUrl(url) {
    const value = String(url || '').trim();
    if (!value) return '';
    if (/^(https?:)?\/\//i.test(value) || value.startsWith('/')) return value;
    return assetPrefix + value.replace(/^\.\//, '');
  }
  function isImagePath(value) {
    return /\.(png|jpe?g|gif|webp|svg)$/i.test(String(value || ''));
  }
  function stickerMarkup(value, className = 'ugfx-chat-sticker') {
    const v = String(value || '').trim();
    if (!v) return '';
    if (isImagePath(v)) {
      return `<div class="${className} ${className}--image"><img src="${escapeHtml(mediaUrl(v))}" alt="Sticker"></div>`;
    }
    return `<div class="${className}">${escapeHtml(v)}</div>`;
  }
  function renderMessages(scroll = false) {
    feed.innerHTML = messages.map(msg => `
      <article class="ugfx-chat-bubble ${escapeHtml(msg.type || 'user')}">
        <div class="ugfx-chat-avatar">${escapeHtml(initialFor(msg.name || 'Guest'))}</div>
        <div class="ugfx-chat-bubble-main">
          <div class="ugfx-chat-bubble-head">
            <strong>${escapeHtml(msg.name || 'Guest')}</strong>
            <span>${escapeHtml(formatTime(msg.created_at || ''))}</span>
          </div>
          ${msg.message ? `<div class="ugfx-chat-text" style="color:${escapeHtml(msg.text_color || '#f3f7ff')}">${autolink(msg.message)}</div>` : ''}
          ${msg.sticker ? stickerMarkup(msg.sticker) : ''}
          ${msg.gif_url ? `<div class="ugfx-chat-media"><img src="${escapeHtml(mediaUrl(msg.gif_url))}" alt="GIF"></div>` : ''}
          ${msg.voice_url ? `<div class="ugfx-chat-voice"><audio controls preload="none" src="${escapeHtml(mediaUrl(msg.voice_url))}"></audio></div>` : ''}
        </div>
      </article>`).join('');
    countEl.textContent = String(messages.length);
    if (scroll) feed.scrollTop = feed.scrollHeight;
  }
  function renderEmojiGrid() {
    const term = (emojiSearch?.value || '').trim().toLowerCase();
    const filtered = EMOJIS.filter(([emoji, cat, keywords]) => {
      const filterOk = currentEmojiFilter === 'all' || cat === currentEmojiFilter;
      const searchOk = !term || keywords.includes(term) || emoji.includes(term);
      return filterOk && searchOk;
    });
    emojiGrid.innerHTML = filtered.map(([emoji]) => `<button type="button" class="ugfx-emoji-btn">${emoji}</button>`).join('');
    emojiGrid.querySelectorAll('.ugfx-emoji-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        messageInput.value += btn.textContent;
        messageInput.focus();
      });
    });
  }
  function filteredMedia(items, state) {
    const term = String(state.search || '').trim().toLowerCase();
    return items.filter(item => {
      const category = String(item.category || 'all').toLowerCase();
      const filterOk = state.filter === 'all' || category === state.filter;
      const haystack = `${item.label || ''} ${item.keywords || ''} ${category}`.toLowerCase();
      const searchOk = !term || haystack.includes(term);
      return filterOk && searchOk;
    });
  }
  function renderFilterChips(host, categories, current, onClick, prefix) {
    if (!host) return;
    host.innerHTML = categories.map(cat => `<button type="button" class="ugfx-mini-chip ${current === cat.id ? 'active' : ''}" data-${prefix}-filter="${escapeHtml(cat.id)}">${escapeHtml(cat.label)}</button>`).join('');
    host.querySelectorAll(`[data-${prefix}-filter]`).forEach(btn => btn.addEventListener('click', () => onClick(btn.getAttribute(`data-${prefix}-filter`) || 'all')));
  }
  function renderStickers() {
    const filtered = filteredMedia(mediaPack.stickers || [], stickerState);
    const visible = filtered.slice(0, stickerState.limit);
    if (stickerCount) stickerCount.textContent = `${visible.length}/${filtered.length}`;
    if (!visible.length) {
      stickerGrid.innerHTML = '<div class="ugfx-media-empty">No stickers match that search.</div>';
    } else {
      stickerGrid.innerHTML = visible.map(item => `
        <button type="button" class="ugfx-sticker-btn ugfx-sticker-btn--media" title="${escapeHtml(item.label)}">
          <img src="${escapeHtml(mediaUrl(item.src))}" alt="${escapeHtml(item.label)}">
          <span>${escapeHtml(item.label)}</span>
        </button>`).join('');
      stickerGrid.querySelectorAll('.ugfx-sticker-btn--media').forEach((btn, idx) => {
        btn.addEventListener('click', () => {
          stickerInput.value = visible[idx].src;
          syncAttachments();
        });
      });
    }
    if (stickerLoadMore) stickerLoadMore.hidden = visible.length >= filtered.length;
  }
  function renderGifPresets() {
    const filtered = filteredMedia(mediaPack.gifs || [], gifState);
    const visible = filtered.slice(0, gifState.limit);
    if (gifCount) gifCount.textContent = `${visible.length}/${filtered.length}`;
    if (!visible.length) {
      gifPresets.innerHTML = '<div class="ugfx-media-empty">No GIFs match that search.</div>';
    } else {
      gifPresets.innerHTML = visible.map(item => `<button type="button" class="ugfx-gif-thumb" title="${escapeHtml(item.label)}"><img src="${escapeHtml(mediaUrl(item.src))}" alt="${escapeHtml(item.label)}"></button>`).join('');
      gifPresets.querySelectorAll('.ugfx-gif-thumb').forEach((btn, idx) => {
        btn.addEventListener('click', () => {
          gifInput.value = visible[idx].src;
          syncAttachments();
        });
      });
    }
    if (gifLoadMore) gifLoadMore.hidden = visible.length >= filtered.length;
  }
  function syncAttachments() {
    const cards = [];
    if (gifInput.value) cards.push(`<div class="ugfx-attach-card ugfx-attach-card--media"><img src="${escapeHtml(mediaUrl(gifInput.value))}" alt="GIF"><span>GIF attached</span><button type="button" data-clear="gif">×</button></div>`);
    if (stickerInput.value) {
      if (isImagePath(stickerInput.value)) {
        cards.push(`<div class="ugfx-attach-card ugfx-attach-card--media"><img src="${escapeHtml(mediaUrl(stickerInput.value))}" alt="Sticker"><span>Sticker attached</span><button type="button" data-clear="sticker">×</button></div>`);
      } else {
        cards.push(`<div class="ugfx-attach-card"><span>${escapeHtml(stickerInput.value)}</span><button type="button" data-clear="sticker">×</button></div>`);
      }
    }
    if (voiceInput.value || selectedVoiceBlob) cards.push(`<div class="ugfx-attach-card"><span>Voice clip ready</span><button type="button" data-clear="voice">×</button></div>`);
    attachments.innerHTML = cards.join('');
    attachments.querySelectorAll('[data-clear]').forEach(btn => {
      btn.addEventListener('click', () => {
        const type = btn.dataset.clear;
        if (type === 'gif') gifInput.value = '';
        if (type === 'sticker') stickerInput.value = '';
        if (type === 'voice') {
          voiceInput.value = '';
          voiceMimeInput.value = '';
          selectedVoiceBlob = null;
          recordStatus.textContent = '';
        }
        syncAttachments();
      });
    });
  }
  function setStatus(msg, isError) {
    statusEl.textContent = msg;
    statusEl.classList.toggle('text-danger', !!isError);
  }
  function openPanel(name) {
    panels.forEach(panel => {
      panel.hidden = panel.dataset.panel !== name ? true : !panel.hidden;
      if (panel.dataset.panel !== name) panel.hidden = true;
    });
    panelButtons.forEach(btn => btn.classList.toggle('active', btn.dataset.panelToggle === name && !app.querySelector(`[data-panel="${name}"]`).hidden));
  }
  function updatePostingBadge() {
    if (!postingBadge) return;
    const currentName = (nameInput.value || '').trim();
    const sameAsAdmin = currentName && adminName && currentName.toLowerCase() === adminName.toLowerCase();
    postingBadge.textContent = sameAsAdmin ? `Posting as admin · ${currentName} (${adminRole})` : `Posting as current DJ · ${currentName || 'Guest DJ'}`;
  }
  async function refreshMessages(scroll = false) {
    try {
      const res = await fetch(getUrl, { cache: 'no-store' });
      const data = await res.json();
      if (data.ok && Array.isArray(data.messages)) {
        messages = data.messages;
        renderMessages(scroll);
      }
    } catch (err) {}
  }
  async function uploadVoiceIfNeeded() {
    if (!selectedVoiceBlob) return true;
    const fd = new FormData();
    fd.append('audio', selectedVoiceBlob, selectedVoiceBlob.name || 'voice.webm');
    recordStatus.textContent = 'Uploading voice clip…';
    const res = await fetch(uploadUrl, { method: 'POST', body: fd });
    const data = await res.json();
    if (!data.ok) throw new Error(data.message || 'Voice upload failed.');
    voiceInput.value = data.url || '';
    voiceMimeInput.value = data.mime || selectedVoiceBlob.type || 'audio/webm';
    recordStatus.textContent = 'Voice clip attached.';
    selectedVoiceBlob = null;
    syncAttachments();
    return true;
  }
  async function loadMediaPack() {
    try {
      const res = await fetch(mediaManifestUrl, { cache: 'no-store' });
      if (!res.ok) throw new Error('manifest');
      const data = await res.json();
      mediaPack = {
        stickers: Array.isArray(data.stickers) ? data.stickers : [],
        gifs: Array.isArray(data.gifs) ? data.gifs : [],
        sticker_categories: Array.isArray(data.sticker_categories) ? data.sticker_categories : [{ id: 'all', label: 'All' }],
        gif_categories: Array.isArray(data.gif_categories) ? data.gif_categories : [{ id: 'all', label: 'All' }]
      };
      renderFilterChips(stickerFiltersHost, mediaPack.sticker_categories, stickerState.filter, (value) => {
        stickerState.filter = value;
        stickerState.limit = 24;
        renderFilterChips(stickerFiltersHost, mediaPack.sticker_categories, stickerState.filter, arguments.callee, 'sticker');
      }, 'sticker');
      renderFilterChips(gifFiltersHost, mediaPack.gif_categories, gifState.filter, (value) => {
        gifState.filter = value;
        gifState.limit = 16;
        renderFilterChips(gifFiltersHost, mediaPack.gif_categories, gifState.filter, arguments.callee, 'gif');
      }, 'gif');
      // Rebind using named wrappers because arguments.callee may be unavailable in strict mode on some hosts.
      bindMediaFilters();
      renderStickers();
      renderGifPresets();
      setStatus(`Chat ready. ${mediaPack.stickers.length} stickers and ${mediaPack.gifs.length} themed GIFs loaded.`, false);
    } catch (err) {
      setStatus('Chat ready. Media pack could not be loaded.', true);
    }
  }
  function bindMediaFilters() {
    renderFilterChips(stickerFiltersHost, mediaPack.sticker_categories, stickerState.filter, (value) => {
      stickerState.filter = value;
      stickerState.limit = 24;
      bindMediaFilters();
      renderStickers();
    }, 'sticker');
    renderFilterChips(gifFiltersHost, mediaPack.gif_categories, gifState.filter, (value) => {
      gifState.filter = value;
      gifState.limit = 16;
      bindMediaFilters();
      renderGifPresets();
    }, 'gif');
  }

  panelButtons.forEach(btn => btn.addEventListener('click', () => openPanel(btn.dataset.panelToggle)));
  if (colorPicker && textColorInput) {
    colorPicker.addEventListener('input', () => { textColorInput.value = colorPicker.value || '#f3f7ff'; });
    textColorInput.value = colorPicker.value || '#f3f7ff';
  }
  emojiFilters.forEach(btn => btn.addEventListener('click', () => {
    currentEmojiFilter = btn.dataset.emojiFilter;
    emojiFilters.forEach(item => item.classList.toggle('active', item === btn));
    renderEmojiGrid();
  }));
  if (emojiSearch) emojiSearch.addEventListener('input', renderEmojiGrid);
  if (stickerSearch) stickerSearch.addEventListener('input', () => {
    stickerState.search = stickerSearch.value || '';
    stickerState.limit = 24;
    renderStickers();
  });
  if (gifSearch) gifSearch.addEventListener('input', () => {
    gifState.search = gifSearch.value || '';
    gifState.limit = 16;
    renderGifPresets();
  });
  if (stickerLoadMore) stickerLoadMore.addEventListener('click', () => {
    stickerState.limit += 24;
    renderStickers();
  });
  if (gifLoadMore) gifLoadMore.addEventListener('click', () => {
    gifState.limit += 16;
    renderGifPresets();
  });
  if (gifAttach) gifAttach.addEventListener('click', () => {
    const url = (gifUrlInput.value || '').trim();
    if (!url) return;
    gifInput.value = url;
    syncAttachments();
    gifUrlInput.value = '';
  });

  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    try {
      setStatus('Sending...', false);
      await uploadVoiceIfNeeded();
      const fd = new FormData(form);
      const res = await fetch(postUrl, { method: 'POST', body: fd });
      const data = await res.json();
      if (!data.ok) throw new Error(data.message || 'Message failed.');
      localStorage.setItem('ugfx-chat-name', nameInput.value.trim());
      form.reset();
      gifInput.value = '';
      stickerInput.value = '';
      voiceInput.value = '';
      voiceMimeInput.value = '';
      selectedVoiceBlob = null;
      syncAttachments();
      nameInput.value = localStorage.getItem('ugfx-chat-name') || '';
      if (colorPicker && textColorInput) textColorInput.value = colorPicker.value || '#f3f7ff';
      updatePostingBadge();
      setStatus('Message sent to chat.', false);
      await refreshMessages(true);
    } catch (err) {
      setStatus(err.message || 'Could not send message.', true);
    }
  });

  if (recordButton && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    recordButton.addEventListener('click', async () => {
      if (recording && mediaRecorder) {
        mediaRecorder.stop();
        return;
      }
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : '';
        mediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
        recorderChunks = [];
        mediaRecorder.ondataavailable = evt => { if (evt.data.size) recorderChunks.push(evt.data); };
        mediaRecorder.onstop = () => {
          const type = mediaRecorder.mimeType || 'audio/webm';
          selectedVoiceBlob = new File(recorderChunks, `voice-${Date.now()}.webm`, { type });
          voiceMimeInput.value = type;
          recording = false;
          recordButton.innerHTML = '<i class="bi bi-mic"></i> Voice';
          recordButton.classList.remove('recording');
          recordStatus.textContent = 'Voice clip ready to send.';
          syncAttachments();
          stream.getTracks().forEach(t => t.stop());
        };
        mediaRecorder.start();
        recording = true;
        recordButton.innerHTML = '<i class="bi bi-stop-circle"></i> Stop';
        recordButton.classList.add('recording');
        recordStatus.textContent = 'Recording… click again to stop.';
      } catch (err) {
        recordStatus.textContent = 'Microphone access blocked.';
      }
    });
  } else if (recordButton) {
    recordButton.disabled = true;
    recordStatus.textContent = 'Voice clips need a newer browser.';
  }

  const savedName = localStorage.getItem('ugfx-chat-name');
  if (savedName) nameInput.value = savedName;
  updatePostingBadge();
  ['change', 'input'].forEach(evt => nameInput.addEventListener(evt, () => {
    localStorage.setItem('ugfx-chat-name', nameInput.value.trim());
    updatePostingBadge();
  }));

  renderMessages(true);
  renderEmojiGrid();
  syncAttachments();
  loadMediaPack();
  setInterval(() => refreshMessages(false), 5000);
})();
