const state = {
  me: null,
  guilds: [],
  channels: [],
  dmConversations: [],
  members: [],
  voicePresence: new Map(),
  selectedGuildId: null,
  selectedTextChannelId: null,
  selectedDmUserId: null,
  selectedDmDisplayName: null,
  selectedVoiceChannelId: null,
  voice: {
    ws: null,
    joinedChannelId: null,
    localStream: null,
    rawStream: null,
    videoStream: null,
    screenStream: null,
    peerConnections: new Map(),
    videoSenders: new Map(),
    screenSenders: new Map(),
    muted: false,
    sharingVideo: false,
    sharingScreen: false,
    deepFilterProcessor: null,
    deepFilterModule: null,
    viewMode: 'chat', // 'chat' or 'video'
    iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
    peerGainNodes: new Map(), // userId -> GainNode
    visibleVolumeSliders: new Set(), // userIds whose sliders are visible
  },
  voicePresencePollId: null,
  chatWs: null,
  lastMessageId: null,
  onlineUsers: new Set(),
  idleUsers: new Set(),
  userVolumes: new Map(), // userId -> volume (0.0 to 2.0)
};

// --- Desktop Backend Configuration ---
let API_BASE_URL = ''; // Will be initialized via IPC
const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId";
const PERSISTED_STORAGE_KEYS = [
  "chattz_token",
  "chattz_refresh_token",
  LAST_GUILD_STORAGE_KEY,
  "active_voice_channel",
];

function storageGet(key) {
  return localStorage.getItem(key);
}

function storageSet(key, value) {
  localStorage.setItem(key, value);
  if (window.electronAPI?.storageSet) {
    void window.electronAPI.storageSet(key, value).catch((err) => {
      console.warn("Failed to persist storage key", key, err);
    });
  }
}

function storageRemove(key) {
  localStorage.removeItem(key);
  if (window.electronAPI?.storageRemove) {
    void window.electronAPI.storageRemove(key).catch((err) => {
      console.warn("Failed to remove persisted storage key", key, err);
    });
  }
}

async function storageSetCritical(key, value) {
  localStorage.setItem(key, value);
  if (window.electronAPI?.storageSet) {
    try {
      await window.electronAPI.storageSet(key, value);
    } catch (err) {
      console.warn("Failed to persist critical storage key", key, err);
    }
  }
}

async function storageRemoveCritical(key) {
  localStorage.removeItem(key);
  if (window.electronAPI?.storageRemove) {
    try {
      await window.electronAPI.storageRemove(key);
    } catch (err) {
      console.warn("Failed to remove critical storage key", key, err);
    }
  }
}

async function hydrateDesktopStorage() {
  if (!window.electronAPI?.storageGet) return;
  for (const key of PERSISTED_STORAGE_KEYS) {
    try {
      // IPC file store (renderer-storage.json) is the source of truth —
      // it always survives restarts, unlike localStorage on file:// URLs.
      const val = await window.electronAPI.storageGet(key);
      if (typeof val === "string") {
        localStorage.setItem(key, val);
      } else {
        // IPC store is empty; backfill from localStorage if available
        const localVal = localStorage.getItem(key);
        if (localVal !== null) {
          await window.electronAPI.storageSet(key, localVal);
        }
      }
    } catch (err) {
      console.warn("Failed to hydrate storage key", key, err);
    }
  }
}

async function initializeConfig() {
  try {
    if (window.electronAPI?.getConfig) {
      const config = await window.electronAPI.getConfig();
      API_BASE_URL = config.backendUrl;
      console.log(`Backend initialized: ${API_BASE_URL}`);
    } else {
      API_BASE_URL = '';
    }
  } catch (err) {
    console.error("Failed to fetch config via IPC", err);
    // Fallback to URL search params if IPC fails (unlikely in desktop app)
    const urlParams = new URLSearchParams(window.location.search);
    API_BASE_URL = urlParams.get('backend') || '';
  } finally {
    // Start the app once config resolution has completed.
    init();
  }
}

function getWsUrl(path) {
  let urlStr;
  if (!API_BASE_URL) {
    const proto = location.protocol === "https:" ? "wss" : "ws";
    urlStr = `${proto}://${location.host}${path}`;
  } else {
    const url = new URL(API_BASE_URL);
    const proto = url.protocol === "https:" ? "wss" : "ws";
    const normalizedPath = path.startsWith('/') ? path : `/${path}`;
    urlStr = `${proto}://${url.host}${normalizedPath}`;
  }

  const token = storageGet("chattz_token");
  if (token) {
    urlStr += (urlStr.includes('?') ? '&' : '?') + `token=${token}`;
  }
  return urlStr;
}






const el = {
  authScreen: document.getElementById("auth-screen"),
  loginBtn: document.getElementById("login-btn"),
  main: document.getElementById("main"),
  status: document.getElementById("status"),

  // Guilds
  guildList: document.getElementById("guild-list"),
  addGuildBtn: document.getElementById("add-guild-btn"),
  guildTitle: document.getElementById("guild-title"),
  createInviteBtn: document.getElementById("create-invite-btn"),
  inviteCopiedBadge: document.getElementById("invite-copied-badge"),

  // Channels
  channelList: document.getElementById("channel-list"),
  voiceChannelList: document.getElementById("voice-channel-list"),
  addTextBtn: document.getElementById("add-text-btn"),
  addVoiceBtn: document.getElementById("add-voice-btn"),
  dmList: document.getElementById("dm-list"),
  channelTitle: document.getElementById("channel-title"),

  // Messages
  messageList: document.getElementById("message-list"),
  messageForm: document.getElementById("message-form"),
  messageBody: document.getElementById("message-body"),
  mediaUploadBtn: document.getElementById("media-upload-btn"),
  mediaFileInput: document.getElementById("media-file-input"),

  // User Panel
  userName: document.getElementById("user-name"),
  userAvatar: document.getElementById("user-avatar"),
  logoutBtn: document.getElementById("logout-btn"),
  updateNotifier: document.getElementById("update-notifier"),
  updateDownloadBtn: document.getElementById("update-download-btn"),
  updateInstallBtn: document.getElementById("update-install-btn"),

  // Voice Connection
  voiceConnection: document.getElementById("voice-connection"),
  vcChannelName: document.getElementById("vc-channel-name"),
  voiceVideoBtn: document.getElementById("voice-video-btn"),
  voiceScreenBtn: document.getElementById("voice-screen-btn"),
  voiceMuteBtn: document.getElementById("voice-mute-btn"),
  voiceLeaveBtn: document.getElementById("voice-leave-btn"),
  videoGrid: document.getElementById("video-grid"),
  messageForm: document.getElementById("message-form"),
  messageInputWrapper: document.querySelector(".chat-input-wrapper"),

  // Members
  memberList: document.getElementById("member-list"),

  // Modals
  modalContainer: document.getElementById("modal-container"),
  guildForm: document.getElementById("guild-form"),
  guildName: document.getElementById("guild-name"),
  modalCancel: document.getElementById("modal-cancel"),

  channelModal: document.getElementById("channel-modal"),
  channelForm: document.getElementById("channel-form"),
  channelName: document.getElementById("channel-name"),
  channelModalCancel: document.getElementById("channel-modal-cancel"),

  // Sound Board
  soundboard: document.getElementById('soundboard'),
  soundboardGrid: document.getElementById('soundboard-grid'),
  addSoundBtn: document.getElementById('add-sound-btn'),
  soundModal: document.getElementById('sound-modal'),
  soundForm: document.getElementById('sound-form'),
  soundName: document.getElementById('sound-name'),
  soundIcon: document.getElementById('sound-icon'),
  soundFile: document.getElementById('sound-file'),
  soundModalCancel: document.getElementById('sound-modal-cancel'),
  soundSubmitBtn: document.getElementById('sound-submit-btn'),

  // Mobile
  mobileMenuBtn: document.getElementById("mobile-menu-btn"),
  mobileMembersBtn: document.getElementById("mobile-members-btn"),
  overlay: document.getElementById("overlay"),
  utilitySidebar: document.getElementById("utility-sidebar"),

  // GIF Picker
  gifBtn: document.getElementById("gif-btn"),
  gifPicker: document.getElementById("gif-picker"),
  gifPickerClose: document.getElementById("gif-picker-close"),
  gifSearchInput: document.getElementById("gif-search-input"),
  gifResults: document.getElementById("gif-results"),
};

// --- API Helpers ---

async function api(path, options = {}) {
  const fullUrl = API_BASE_URL ? (path.startsWith('http') ? path : `${API_BASE_URL}${path}`) : path;

  const headers = {
    "content-type": "application/json",
    ...(options.headers || {}),
  };

  const token = storageGet("chattz_token");
  if (token) {
    headers["Authorization"] = `Bearer ${token}`;
  }

  let res = await fetch(fullUrl, {
    ...options,
    headers,
  });

  // Handle 401 Unauthorized via Refresh Token
  if (res.status === 401 && !path.includes('/auth/refresh')) {
    const refreshToken = storageGet("chattz_refresh_token");
    if (refreshToken) {
      try {
        const refreshUrl = API_BASE_URL ? `${API_BASE_URL}/auth/refresh` : '/auth/refresh';
        const refreshRes = await fetch(refreshUrl, {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ refresh_token: refreshToken })
        });

        if (refreshRes.ok) {
          const newTokens = await refreshRes.json();
          await storageSetCritical("chattz_token", newTokens.access_token);
          await storageSetCritical("chattz_refresh_token", newTokens.refresh_token);

          // Retry original request with new token
          headers["Authorization"] = `Bearer ${newTokens.access_token}`;
          res = await fetch(fullUrl, { ...options, headers });
        } else {
          // Both tokens invalid/expired, wipe out to force clear state
          throw new Error("Refresh token expired or invalid");
        }
      } catch (err) {
        await storageRemoveCritical("chattz_token");
        await storageRemoveCritical("chattz_refresh_token");
        location.href = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/auth/login";
        throw new Error("Session expired, please log in again");
      }
    }
  }

  if (!res.ok) {
    let detail = "request failed";
    try {
      const body = await res.json();
      detail = body.error || detail;
    } catch { }
    throw new Error(`${res.status}: ${detail}`);
  }

  if (res.status === 204) return null;
  return res.json();
}

// --- Utils ---

function shortName(name) {
  if (!name || typeof name !== 'string') return "?";
  const trimmed = name.trim();
  if (!trimmed) return "?";
  const words = trimmed.split(/\s+/).filter(w => w.length > 0).slice(0, 2);
  if (words.length === 0) return "?";
  if (words.length === 1) return words[0].substring(0, 2).toUpperCase();
  return words.map((w) => w[0]?.toUpperCase() || "").join("");
}

function escapeHtml(s) {
  if (s === null || s === undefined) return "";
  return String(s)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function formatMessageBody(body) {
  const escaped = escapeHtml(body);
  // Detect GIF URLs (simple regex for demo)
  const urlRegex = /(https?:\/\/[^\s]+(?:\.gif|\/giphy\.gif)[^\s]*)/gi;
  if (urlRegex.test(body)) {
    return escaped.replace(urlRegex, (url) => {
      return `<div class="msg-gif"><img src="${url}" loading="lazy" /></div>`;
    });
  }
  return escaped;
}

function formatBytes(size) {
  if (!Number.isFinite(size) || size < 1024) return `${size || 0} B`;
  const units = ["KB", "MB", "GB"];
  let value = size;
  let unitIndex = -1;
  while (value >= 1024 && unitIndex < units.length - 1) {
    value /= 1024;
    unitIndex += 1;
  }
  return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
}

function renderAttachments(attachments = []) {
  if (!attachments.length) return "";

  return attachments.map((attachment) => {
    const url = escapeHtml(attachment.media_url);
    const mime = attachment.mime_type || "application/octet-stream";
    const fileName = escapeHtml(attachment.original_filename || "file");
    const size = formatBytes(attachment.size_bytes);

    if (mime.startsWith("image/")) {
      return `<a class="msg-attachment msg-attachment-image" href="${url}" target="_blank" rel="noopener noreferrer"><img src="${url}" alt="${fileName}" loading="lazy" /></a>`;
    }

    if (mime.startsWith("video/")) {
      return `<div class="msg-attachment msg-attachment-video"><video controls preload="metadata" src="${url}"></video></div>`;
    }

    if (mime.startsWith("audio/")) {
      return `<div class="msg-attachment msg-attachment-audio"><audio controls preload="metadata" src="${url}"></audio><a href="${url}" target="_blank" rel="noopener noreferrer">${fileName}</a></div>`;
    }

    return `<a class="msg-attachment msg-attachment-file" href="${url}" target="_blank" rel="noopener noreferrer"><i data-lucide="file"></i><span>${fileName}</span><small>${size}</small></a>`;
  }).join("");
}

function formatDate(isoString) {
  const d = new Date(isoString);
  const now = new Date();
  const isToday = d.toDateString() === now.toDateString();

  if (isToday) {
    return `Today at ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
  }
  return d.toLocaleDateString();
}

function scrollMessagesToBottom() {
  if (!el.messageList) return;
  el.messageList.scrollTop = el.messageList.scrollHeight;
}

let audioCtx = null;

// Global interaction listener to unlock AudioContext (autoplay policy)
window.addEventListener('click', () => {
  if (audioCtx && audioCtx.state === 'suspended') {
    audioCtx.resume();
  }
}, { once: true });

function playSound(type) {
  try {
    if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    if (audioCtx.state === 'suspended') audioCtx.resume();

    const sounds = {
      message: { freq: 880, type: 'sine', duration: 0.1, volume: 0.1 },
      dm: { freq: [660, 880], type: 'sine', duration: 0.15, volume: 0.1 },
      join: { freq: [440, 880], type: 'sine', duration: 0.2, volume: 0.1 },
      leave: { freq: [880, 440], type: 'sine', duration: 0.2, volume: 0.1 },
      'peer-join': { freq: [660, 990], type: 'sine', duration: 0.15, volume: 0.05 },
      'peer-leave': { freq: [990, 660], type: 'sine', duration: 0.15, volume: 0.05 },
    };

    const s = sounds[type];
    if (!s) return;

    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();

    osc.type = s.type;
    if (Array.isArray(s.freq)) {
      osc.frequency.setValueAtTime(s.freq[0], audioCtx.currentTime);
      osc.frequency.exponentialRampToValueAtTime(s.freq[1], audioCtx.currentTime + s.duration);
    } else {
      osc.frequency.setValueAtTime(s.freq, audioCtx.currentTime);
    }

    gain.gain.setValueAtTime(s.volume, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + s.duration);

    osc.connect(gain);
    gain.connect(audioCtx.destination);

    osc.start();
    osc.stop(audioCtx.currentTime + s.duration);
  } catch (err) {
    console.warn("playSound failed", err);
  }
}
// --- Renderers ---

function renderGuilds() {
  el.guildList.innerHTML = "";

  for (const guild of state.guilds) {
    const btn = document.createElement("button");
    btn.className = `guild-pill ${state.selectedGuildId === guild.id ? "active" : ""}`;
    btn.title = guild.name;
    btn.textContent = shortName(guild.name);
    btn.onclick = async () => {
      state.selectedGuildId = guild.id;
      state.selectedTextChannelId = null;
      state.selectedVoiceChannelId = null;
      state.selectedDmUserId = null;
      state.selectedDmDisplayName = null;
      storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
      renderGuilds();
      renderDMs();
      await loadChannels();
      await loadGuildMembers();
      await refreshVoicePresence();
      startVoicePresencePolling();
      renderMessages([]);
      updateHeaderLabels();
    };
    el.guildList.appendChild(btn);
  }
}

function renderChannels() {
  el.channelList.innerHTML = "";
  el.voiceChannelList.innerHTML = "";

  for (const channel of state.channels) {
    const row = document.createElement("button");
    row.className = `channel-row ${(channel.kind === 'text' && state.selectedTextChannelId === channel.id) ||
      (channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : ""
      }`;

    const iconName = channel.kind === 'text' ? 'hash' : 'volume-2';
    row.innerHTML = `<i data-lucide="${iconName}"></i> <span>${escapeHtml(channel.name)}</span>`;

    row.onclick = async () => {
      if (channel.kind === 'text') {
        state.selectedDmUserId = null;
        state.selectedDmDisplayName = null;
        state.selectedTextChannelId = channel.id;
        state.voice.viewMode = 'chat';
        renderChannels();
        renderDMs();
        updateView();
        updateHeaderLabels();
        const messages = await api(`/channels/${channel.id}/messages?limit=100`);
        renderMessages(messages);
      } else {
        state.selectedVoiceChannelId = channel.id;
        state.voice.viewMode = 'video';
        renderChannels();
        renderDMs();
        updateView();
        updateHeaderLabels();
        try {
          await joinVoice();
        } catch (err) {
          console.error("joinVoice failed from channel click", err);
          alert(`Voice join failed: ${err?.message || err}`);
        }
      }
      if (window.innerWidth <= 768) closeMobileMenus();
    };

    if (channel.kind === 'text') {
      el.channelList.appendChild(row);
    } else {
      el.voiceChannelList.appendChild(row);

      const participants = state.voicePresence.get(channel.id) || [];
      if (participants.length > 0) {
        const pList = document.createElement("div");
        pList.className = "voice-row-members channel-list";
        pList.style.paddingLeft = "24px";
        for (const p of participants) {
          const pRow = document.createElement("div");
          pRow.className = `channel-row ${p.is_speaking ? 'voice-speaking' : ''}`;
          pRow.style.padding = "2px 8px";
          pRow.style.flexWrap = "wrap";

          let sliderHtml = '';
          if (p.user_id !== state.me.id) {
            const vol = state.userVolumes.get(p.user_id) ?? 1.0;
            const isVisible = state.voice.visibleVolumeSliders.has(p.user_id);
            sliderHtml = `
              <div class="user-volume-control ${isVisible ? 'show-volume' : ''}" data-user-id="${p.user_id}">
                <i data-lucide="volume-2" style="width: 12px; height: 12px; opacity: 0.6;"></i>
                <input type="range" min="0" max="2" step="0.1" value="${vol}" class="volume-slider">
                <span class="vol-pct">${Math.round(vol * 100)}%</span>
              </div>
            `;
          }

          pRow.innerHTML = `
            <div style="display: flex; align-items: center; gap: 8px; width: 100%;">
              <div class="avatar" style="width:20px;height:20px;font-size:10px">${shortName(p.display_name)}</div> 
              <span style="flex: 1; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(p.display_name)}</span>
              ${p.is_muted ? '<i data-lucide="mic-off" class="voice-muted-icon"></i>' : ''}
            </div>
            ${sliderHtml}
          `;

          const slider = pRow.querySelector('.volume-slider');
          if (slider) {
            slider.addEventListener('input', (e) => {
              const val = parseFloat(e.target.value);
              state.userVolumes.set(p.user_id, val);
              pRow.querySelector('.vol-pct').textContent = `${Math.round(val * 100)}%`;

              const gainNode = state.voice.peerGainNodes.get(p.user_id);
              if (gainNode) {
                gainNode.gain.setTargetAtTime(val, state.voice.audioContext.currentTime, 0.05);
              }
            });
            // Stop propagation to prevent joining channel again when clicking slider
            slider.addEventListener('click', (e) => e.stopPropagation());
          }

          const toggleVolumeSlider = (e) => {
            if (p.user_id === state.me.id) return;
            e.preventDefault();
            e.stopPropagation();
            if (state.voice.visibleVolumeSliders.has(p.user_id)) {
              state.voice.visibleVolumeSliders.delete(p.user_id);
            } else {
              state.voice.visibleVolumeSliders.add(p.user_id);
            }
            renderChannels();
          };

          // Electron can swallow contextmenu events on some platforms; use
          // right-button mousedown as a reliable fallback for slider toggle.
          pRow.addEventListener('contextmenu', toggleVolumeSlider);
          pRow.addEventListener('mousedown', (e) => {
            if (e.button === 2) toggleVolumeSlider(e);
          });

          pList.appendChild(pRow);
        }
        el.voiceChannelList.appendChild(pList);
      }
    }
  }
  lucide.createIcons();
}

function renderDMs() {
  el.dmList.innerHTML = "";
  for (const dm of state.dmConversations) {
    const isOnline = state.onlineUsers.has(dm.user_id);
    const isIdle = state.idleUsers.has(dm.user_id);
    const row = document.createElement("button");
    row.className = `channel-row ${state.selectedDmUserId === dm.user_id ? "active" : ""}`;
    row.innerHTML = `
      <div class="avatar-wrapper">
        <i data-lucide="message-circle"></i>
        <div class="status-dot ${isOnline ? (isIdle ? 'idle' : 'online') : ''}"></div>
      </div>
      <span>${escapeHtml(dm.display_name)}</span>
    `;
    row.onclick = async () => {
      state.selectedDmUserId = dm.user_id;
      state.selectedDmDisplayName = dm.display_name;
      state.selectedTextChannelId = null;
      state.selectedVoiceChannelId = null;
      state.voice.viewMode = 'chat';
      renderChannels();
      renderDMs();
      updateView();
      updateHeaderLabels();
      const messages = await api(`/dms/${dm.user_id}/messages?limit=100`);
      renderMessages(messages);
    };
    el.dmList.appendChild(row);
  }

  lucide.createIcons();
}

function renderMessages(messages) {
  el.messageList.innerHTML = "";
  let lastAuthorId = null;
  let lastTime = null;

  for (const m of messages.slice().reverse()) {
    const row = document.createElement("div");
    const mDate = new Date(m.created_at);
    const isGrouped = lastAuthorId === m.author_user_id &&
      lastTime && (mDate - lastTime < 300000); // 5 minutes

    row.className = `msg ${isGrouped ? "msg-grouped" : ""}`;

    const displayName = m.author_display_name || "Unknown User";
    const formattedBody = formatMessageBody(m.body || "");
    const attachmentsHtml = renderAttachments(m.attachments);
    const bodyHtml = formattedBody ? `<div class="msg-body">${formattedBody}</div>` : "";
    const contentHtml = `${bodyHtml}${attachmentsHtml}`;

    if (isGrouped) {
      row.innerHTML = `<div class="msg-content">${contentHtml}</div>`;
    } else {
      row.innerHTML = `
        <div class="msg-avatar">${shortName(displayName)}</div>
        <div class="msg-content">
          <div class="msg-header">
            <span class="msg-author">${escapeHtml(displayName)}</span>
            <span class="msg-time">${formatDate(m.created_at)}</span>
          </div>
          ${contentHtml}
        </div>
      `;
    }
    el.messageList.appendChild(row);
    lastAuthorId = m.author_user_id;
    lastTime = mDate;
  }

  if (messages.length > 0) {
    state.lastMessageId = messages[0].id;
  }

  scrollMessagesToBottom();
  requestAnimationFrame(() => {
    scrollMessagesToBottom();
  });

  // GIF/image height resolves after initial paint; keep chat pinned to latest.
  for (const gifImage of el.messageList.querySelectorAll(".msg-gif img")) {
    if (gifImage.complete) continue;
    gifImage.addEventListener("load", scrollMessagesToBottom, { once: true });
    gifImage.addEventListener("error", scrollMessagesToBottom, { once: true });
  }

  for (const media of el.messageList.querySelectorAll(".msg-attachment img, .msg-attachment video, .msg-attachment audio")) {
    if ("complete" in media && media.complete) continue;
    media.addEventListener("loadeddata", scrollMessagesToBottom, { once: true });
    media.addEventListener("load", scrollMessagesToBottom, { once: true });
    media.addEventListener("error", scrollMessagesToBottom, { once: true });
  }

  lucide.createIcons();
}

function renderMembers() {
  el.memberList.innerHTML = "";
  for (const m of state.members) {
    const isOnline = state.onlineUsers.has(m.id);
    const isIdle = state.idleUsers.has(m.id);
    const row = document.createElement("div");
    row.className = "member-row";
    row.innerHTML = `
      <div class="member-avatar">
        ${shortName(m.display_name)}
        <div class="status-dot ${isOnline ? (isIdle ? 'idle' : 'online') : ''}"></div>
      </div>
      <div class="member-name">${escapeHtml(m.display_name)}</div>
    `;
    row.style.cursor = "pointer";
    row.onclick = async () => {
      if (state.me && m.id === state.me.id) return;
      state.selectedDmUserId = m.id;
      state.selectedDmDisplayName = m.display_name;
      state.selectedTextChannelId = null;
      state.selectedVoiceChannelId = null;
      state.voice.viewMode = 'chat';
      renderChannels();
      renderDMs();
      updateView();
      updateHeaderLabels();
      const messages = await api(`/dms/${m.id}/messages?limit=100`);
      renderMessages(messages);
    };
    el.memberList.appendChild(row);
  }
}

function updateHeaderLabels() {
  const guild = state.guilds.find((g) => g.id === state.selectedGuildId);
  el.guildTitle.textContent = guild ? guild.name : "No server selected";

  if (state.selectedDmUserId) {
    const dmFromConversations = state.dmConversations.find((u) => u.user_id === state.selectedDmUserId);
    const dmFromMembers = state.members.find((m) => m.id === state.selectedDmUserId);
    const displayName = dmFromConversations?.display_name || dmFromMembers?.display_name || state.selectedDmDisplayName;
    const dmName = displayName ? `@${displayName}` : "Direct Message";
    el.channelTitle.textContent = dmName;
    el.messageBody.placeholder = displayName
      ? `Message @${displayName}`
      : "Message";
    return;
  }

  if (state.voice.viewMode === 'video' && state.selectedVoiceChannelId) {
    const channel = state.channels.find((c) => c.id === state.selectedVoiceChannelId);
    el.channelTitle.textContent = channel ? channel.name : "Voice Arena";
    return;
  }

  const channel = state.channels.find((c) => c.id === state.selectedTextChannelId);
  el.channelTitle.textContent = channel ? channel.name : "Select a channel";
  el.messageBody.placeholder = channel ? `Message #${channel.name}` : "Select a channel";
}

async function createInviteLink() {
  if (!state.selectedGuildId) {
    alert("Select a server first.");
    return;
  }
  try {
    const invite = await api(`/guilds/${state.selectedGuildId}/invites`, {
      method: "POST",
      body: JSON.stringify({ max_uses: 50, expires_in_hours: 24 }),
    });
    // Use the backend URL if available, otherwise fallback to current origin
    const base = API_BASE_URL ? new URL(API_BASE_URL).origin : location.origin;
    const link = `${base}/?invite=${encodeURIComponent(invite.code)}`;

    // Use Electron's native clipboard API if available (defined in preload.js)
    if (window.electronAPI && window.electronAPI.copyToClipboard) {
      try {
        await window.electronAPI.copyToClipboard(link);
        showCopiedBadge();
        return;
      } catch (e) {
        console.error("Electron clipboard write failed:", e);
      }
    }

    // Fallback to navigator.clipboard
    try {
      await navigator.clipboard.writeText(link);
      showCopiedBadge();
    } catch (err) {
      console.error("Navigator clipboard write failed:", err);
      prompt("Copy invite link:", link);
    }
  } catch (err) {
    alert(err.message);
  }
}

function showCopiedBadge() {
  el.inviteCopiedBadge.classList.remove("hidden");
  setTimeout(() => {
    el.inviteCopiedBadge.classList.add("hidden");
  }, 2000);
}

// --- Logic ---

async function loadGuilds() {
  state.guilds = await api("/guilds");
  renderGuilds();
}

async function loadDMConversations() {
  state.dmConversations = await api("/dms");
  renderDMs();
}

async function loadGuildMembers() {
  if (!state.selectedGuildId) {
    state.members = [];
    renderMembers();
    return;
  }
  state.members = await api(`/guilds/${state.selectedGuildId}/members`);
  renderMembers();
}

async function loadChannels() {
  if (!state.selectedGuildId) {
    state.channels = [];
    renderChannels();
    return;
  }

  state.channels = await api(`/guilds/${state.selectedGuildId}/channels`);
  renderChannels();
}

async function refreshVoicePresence() {
  if (!state.selectedGuildId) {
    state.voicePresence.clear();
    renderChannels();
    return;
  }

  try {
    const res = await api(`/guilds/${state.selectedGuildId}/voice-presence`);
    state.voicePresence.clear();
    for (const entry of res.channels || []) {
      state.voicePresence.set(entry.channel_id, entry.participants || []);
    }
    renderChannels();
  } catch (err) {
    console.warn("voice presence failed", err);
  }
}

function startVoicePresencePolling() {
  if (state.voicePresencePollId) clearInterval(state.voicePresencePollId);
  state.voicePresencePollId = setInterval(() => {
    refreshVoicePresence().catch(() => { });
  }, 3000);
}

function initChatWs() {
  if (state.chatWs) state.chatWs.close();
  const wsUrl = getWsUrl('/ws');
  const ws = new WebSocket(wsUrl);
  state.chatWs = ws;

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    const authorId = msg.message?.author_user_id;
    const isFromMe = authorId && state.me && authorId === state.me.id;

    if (msg.type === "message_created") {
      if (!isFromMe) playSound('message');
      if (state.selectedTextChannelId === msg.channel_id) {
        api(`/channels/${state.selectedTextChannelId}/messages?limit=100`).then(renderMessages);
      }
    } else if (msg.type === "dm_created") {
      if (!isFromMe) playSound('dm');
      if (state.selectedDmUserId === msg.other_user_id) {
        api(`/dms/${state.selectedDmUserId}/messages?limit=100`).then(renderMessages);
        playSound('dm');
        loadDMConversations().then(renderDMs);
      } else {
        loadDMConversations().then(renderDMs);
      }
    } else if (msg.type === "user_presence") {
      if (msg.online) {
        state.onlineUsers.add(msg.user_id);
        if (msg.idle) {
          state.idleUsers.add(msg.user_id);
        } else {
          state.idleUsers.delete(msg.user_id);
        }
      } else {
        state.onlineUsers.delete(msg.user_id);
        state.idleUsers.delete(msg.user_id);
      }
      renderMembers();
      renderDMs();
    }
  };

  ws.onclose = () => {
    console.log("Chat WS closed, reconnecting...");
    setTimeout(initChatWs, 3000);
  };
}

// --- Voice ---

function getVoiceWsUrl(channelId) {
  return getWsUrl(`/channels/${channelId}/voice/ws`);
}

function shouldInitiateOffer(peerId) {
  if (!state.me || !state.me.id) return false;
  return state.me.id > peerId;
}

function stopAndClearAudioPipeline() {
  if (state.voice.deepFilterProcessor) {
    state.voice.deepFilterProcessor.destroy();
    state.voice.deepFilterProcessor = null;
  }
  if (state.voice.localStream) {
    for (const track of state.voice.localStream.getTracks()) track.stop();
  }
  if (state.voice.rawStream && state.voice.rawStream !== state.voice.localStream) {
    for (const track of state.voice.rawStream.getTracks()) track.stop();
  }
  if (state.voice.audioContext) {
    state.voice.audioContext.close().catch(() => { });
  }
  state.voice.localStream = null;
  state.voice.rawStream = null;
  state.voice.audioContext = null;
}

function stopAndClearVideoPipeline() {
  if (state.voice.videoStream) {
    for (const track of state.voice.videoStream.getTracks()) track.stop();
  }
  state.voice.videoStream = null;
  state.voice.sharingVideo = false;
  document.getElementById(`video-${state.me.id}-camera`)?.parentElement?.remove();
}

function stopAndClearScreenPipeline() {
  if (state.voice.screenStream) {
    for (const track of state.voice.screenStream.getTracks()) track.stop();
  }
  state.voice.screenStream = null;
  state.voice.sharingScreen = false;
  document.getElementById(`video-${state.me.id}-screen`)?.parentElement?.remove();
}

function updateVideoGridVisibility() {
  // Persistence logic: we always update the grid visibility based on participants
  // however the actual container visibility in the UI is managed by updateView()
}

function updateView() {
  if (state.voice.viewMode === 'video') {
    el.videoGrid.classList.remove("hidden");
    el.messageList.classList.add("hidden");
    el.messageInputWrapper.classList.add("hidden");
  } else {
    el.videoGrid.classList.add("hidden");
    el.messageList.classList.remove("hidden");
    el.messageInputWrapper.classList.remove("hidden");
  }
}

function renderVideo(peerId, displayName, stream, source) {
  const videoId = `video-${peerId}-${source}`;
  let videoEl = document.getElementById(videoId);
  if (!videoEl) {
    const container = document.createElement("div");
    container.className = "video-item";
    const label = source === 'screen' ? `${escapeHtml(displayName)}'s Screen` : escapeHtml(displayName);
    container.innerHTML = `
      <video id="${videoId}" autoplay playsinline ${peerId === state.me.id ? "muted" : ""}></video>
      <div class="video-label">${label}</div>
    `;
    container.onclick = () => {
      const isFullscreen = container.classList.contains('fullscreen');
      // Reset all
      document.querySelectorAll('.video-item').forEach(el => el.classList.remove('fullscreen'));

      if (isFullscreen) {
        el.videoGrid.classList.remove('has-fullscreen');
      } else {
        container.classList.add('fullscreen');
        el.videoGrid.classList.add('has-fullscreen');
      }
    };

    el.videoGrid.appendChild(container);
    videoEl = container.querySelector("video");
  }
  videoEl.srcObject = stream;
}

async function buildAudioPipeline(rawStream) {
  const ctx = new AudioContext();
  state.voice.audioContext = ctx;
  const source = ctx.createMediaStreamSource(rawStream);
  let processedSource = source;

  try {
    const deepFilter = await getDeepFilterModule();
    const processor = new deepFilter.DeepFilterNet3Core({
      sampleRate: 48000,
      noiseReductionLevel: 70,
      assetConfig: {
        cdnUrl: resolveDeepFilterAssetsBaseUrl(),
      },
    });
    await processor.initialize();
    const workletNode = await processor.createAudioWorkletNode(ctx);
    processor.setNoiseSuppressionEnabled(true);
    source.connect(workletNode);
    processedSource = workletNode;
    state.voice.deepFilterProcessor = processor;
    console.info('DeepFilterNet3 enabled');
  } catch (err) {
    console.warn('DeepFilterNet3 unavailable, falling back to raw mic audio', err);
    state.voice.deepFilterProcessor = null;
  }

  // Metering/Speaking detection with adaptive threshold and hysteresis.
  const analyser = ctx.createAnalyser();
  analyser.fftSize = 1024;
  processedSource.connect(analyser);

  const supportsFloatTimeDomain = typeof analyser.getFloatTimeDomainData === 'function';
  const timeData = supportsFloatTimeDomain ? new Float32Array(analyser.fftSize) : new Uint8Array(analyser.fftSize);
  let localIsSpeaking = false;
  let speechFrames = 0;
  let silenceFrames = 0;
  let noiseFloor = 0.004;
  let levelEma = 0;

  const checkVolume = () => {
    if (!state.voice.audioContext || state.voice.audioContext.state === 'closed') return;

    try {
      if (supportsFloatTimeDomain) {
        analyser.getFloatTimeDomainData(timeData);
      } else {
        analyser.getByteTimeDomainData(timeData);
      }
    } catch (err) {
      setTimeout(checkVolume, 60);
      return;
    }

    let sumSquares = 0;
    let peak = 0;
    for (let i = 0; i < timeData.length; i++) {
      const v = supportsFloatTimeDomain
        ? timeData[i]
        : (timeData[i] - 128) / 128;
      sumSquares += v * v;
      const abs = Math.abs(v);
      if (abs > peak) peak = abs;
    }

    const rms = Math.sqrt(sumSquares / timeData.length);
    const level = Math.max(rms, peak * 0.5);
    levelEma = levelEma * 0.75 + level * 0.25;

    if (!localIsSpeaking) {
      // Learn room noise slowly while idle.
      noiseFloor = noiseFloor * 0.98 + levelEma * 0.02;
    } else {
      // Do not let noise floor jump up while speaking.
      noiseFloor = Math.min(noiseFloor, levelEma);
    }

    const startThreshold = Math.max(noiseFloor * 2.2, 0.010);
    const stopThreshold = Math.max(noiseFloor * 1.5, 0.006);

    if (levelEma > startThreshold) {
      speechFrames = Math.min(speechFrames + 1, 8);
      silenceFrames = 0;
    } else if (levelEma < stopThreshold) {
      silenceFrames = Math.min(silenceFrames + 1, 8);
      speechFrames = Math.max(speechFrames - 1, 0);
    }

    const newSpeakingState = localIsSpeaking ? silenceFrames < 3 : speechFrames >= 2;

    if (newSpeakingState !== localIsSpeaking) {
      localIsSpeaking = newSpeakingState;
      if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) {
        state.voice.ws.send(JSON.stringify({ type: 'set_speaking_status', is_speaking: localIsSpeaking }));
      }
      // Local UI update
      state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => {
        if (p.user_id === state.me.id) p.is_speaking = localIsSpeaking;
      });
      renderChannels();
      const myVideo = document.getElementById(`video-${state.me.id}-camera`) || document.getElementById(`video-${state.me.id}-screen`);
      if (myVideo) {
        myVideo.parentElement.classList.toggle('speaking', localIsSpeaking);
      }
    }
    setTimeout(checkVolume, 60);
  };
  checkVolume();

  const destination = ctx.createMediaStreamDestination();
  processedSource.connect(destination);
  return destination.stream;
}

function resolveDeepFilterModuleUrl() {
  if (location.protocol === 'file:') {
    return new URL('./vendor/deepfilternet3-noise-filter.esm.js', location.href).toString();
  }
  return '/static/vendor/deepfilternet3-noise-filter.esm.js';
}

function resolveDeepFilterAssetsBaseUrl() {
  if (location.protocol === 'file:') {
    return new URL('./vendor/deepfilternet3', location.href).toString();
  }
  return '/static/vendor/deepfilternet3';
}

async function getDeepFilterModule() {
  if (state.voice.deepFilterModule) return state.voice.deepFilterModule;
  const moduleUrl = resolveDeepFilterModuleUrl();
  state.voice.deepFilterModule = await import(moduleUrl);
  return state.voice.deepFilterModule;
}

async function createLocalVoiceStream() {
  const constraints = {
    audio: {
      channelCount: 1,
      sampleRate: 48000,
      echoCancellation: true,
      noiseSuppression: false,
      autoGainControl: false,
    },
    video: false,
  };
  const rawStream = await navigator.mediaDevices.getUserMedia(constraints);
  let localStream = rawStream;
  try {
    localStream = await buildAudioPipeline(rawStream);
  } catch (err) {
    console.warn('voice audio pipeline failed, using raw mic stream', err);
  }
  state.voice.rawStream = rawStream;
  state.voice.localStream = localStream;
  if (state.voice.muted) {
    state.voice.localStream.getAudioTracks().forEach((t) => {
      t.enabled = false;
    });
  }
}

function ensurePeerConnection(peerId) {
  if (state.voice.peerConnections.has(peerId)) {
    return state.voice.peerConnections.get(peerId);
  }

  const pc = new RTCPeerConnection({
    iceServers: state.voice.iceServers
  });

  pc.makingOffer = false;
  pc.ignoreOffer = false;
  pc.polite = !shouldInitiateOffer(peerId);

  // 1. Audio
  if (state.voice.localStream) {
    state.voice.localStream.getAudioTracks().forEach(track => {
      pc.addTrack(track, state.voice.localStream);
    });
  } else {
    pc.addTransceiver('audio', { direction: 'recvonly' });
  }

  // 2. Camera Video
  if (state.voice.videoStream) {
    state.voice.videoStream.getVideoTracks().forEach(track => {
      const sender = pc.addTrack(track, state.voice.videoStream);
      state.voice.videoSenders.set(peerId, sender);
    });
  }

  // 3. Screen Video
  if (state.voice.screenStream) {
    state.voice.screenStream.getVideoTracks().forEach(track => {
      const sender = pc.addTrack(track, state.voice.screenStream);
      state.voice.screenSenders.set(peerId, sender);
    });
  }

  pc.onicecandidate = (event) => {
    if (!event.candidate || !state.voice.ws) return;
    state.voice.ws.send(JSON.stringify({
      type: "signal",
      to_user_id: peerId,
      kind: "ice",
      data: event.candidate,
    }));
  };

  pc.oniceconnectionstatechange = async () => {
    const iceState = pc.iceConnectionState;
    console.debug(`voice ice state (${peerId}):`, iceState);
    if (iceState !== "failed" || pc.restartingIce) return;
    pc.restartingIce = true;
    try {
      const offer = await pc.createOffer({ iceRestart: true });
      await pc.setLocalDescription(offer);
      if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) {
        state.voice.ws.send(JSON.stringify({
          type: "signal",
          to_user_id: peerId,
          kind: "offer",
          data: pc.localDescription,
        }));
      }
    } catch (err) {
      console.warn("ICE restart failed", err);
    } finally {
      pc.restartingIce = false;
    }
  };

  pc.ontrack = (event) => {
    const stream = event.streams[0];
    if (event.track.kind === "audio") {
      let audio = document.getElementById(`audio-${peerId}`);
      if (!audio) {
        audio = document.createElement("audio");
        audio.id = `audio-${peerId}`;
        audio.autoplay = true;
        audio.playsInline = true;
        document.body.appendChild(audio);
      }
      audio.muted = false;
      audio.srcObject = new MediaStream([event.track]);
      const tryPlay = () => {
        const playPromise = audio.play();
        if (playPromise && typeof playPromise.catch === "function") {
          playPromise.catch((err) => {
            console.warn(`remote audio playback blocked for ${peerId}`, err);
          });
        }
      };
      audio.onloadedmetadata = tryPlay;
      audio.oncanplay = tryPlay;
      tryPlay();

      // Web Audio Pipeline for Volume Boosting
      if (!state.voice.audioContext) {
        state.voice.audioContext = new (window.AudioContext || window.webkitAudioContext)();
      }
      const ctx = state.voice.audioContext;
      if (ctx.state === 'suspended') ctx.resume();

      const sourceNode = ctx.createMediaStreamSource(audio.srcObject);
      const gainNode = ctx.createGain();

      const currentVolume = state.userVolumes.get(peerId) ?? 1.0;
      gainNode.gain.setValueAtTime(currentVolume, ctx.currentTime);

      sourceNode.connect(gainNode);
      gainNode.connect(ctx.destination);

      state.voice.peerGainNodes.set(peerId, gainNode);

      // Mute the original element as we play through Web Audio destination
      audio.volume = 0;
      audio.muted = true;
    } else if (event.track.kind === "video") {
      const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" };
      renderVideo(peerId, peer.display_name, stream, event.track.id);

      event.track.onmute = () => {
        document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove();
      };
      event.track.onended = () => {
        document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove();
      };
    }
  };

  pc.onnegotiationneeded = async () => {
    try {
      pc.makingOffer = true;
      const offer = await pc.createOffer();
      await pc.setLocalDescription(offer);
      state.voice.ws.send(JSON.stringify({
        type: "signal",
        to_user_id: peerId,
        kind: "offer",
        data: pc.localDescription,
      }));
    } catch (err) {
      console.error("negotiation failed", err);
    } finally {
      pc.makingOffer = false;
    }
  };

  state.voice.peerConnections.set(peerId, pc);
  return pc;
}

async function attachLocalAudioToPeerConnections() {
  if (!state.voice.localStream) return;
  const localTrack = state.voice.localStream.getAudioTracks()[0];
  if (!localTrack) return;

  await Promise.all(
    Array.from(state.voice.peerConnections.values()).map(async (pc) => {
      const audioTransceiver = pc.getTransceivers().find((t) => t.receiver?.track?.kind === 'audio');
      if (audioTransceiver?.sender) {
        await audioTransceiver.sender.replaceTrack(localTrack);
        if (audioTransceiver.direction === 'recvonly') {
          audioTransceiver.direction = 'sendrecv';
        }
      } else {
        pc.addTrack(localTrack, state.voice.localStream);
      }
    })
  );
}

async function handleSignal(fromPeerId, kind, data) {
  const pc = ensurePeerConnection(fromPeerId);

  try {
    if (kind === "offer") {
      const offerCollision = pc.makingOffer || pc.signalingState !== "stable";
      pc.ignoreOffer = !pc.polite && offerCollision;
      if (pc.ignoreOffer) return;

      await pc.setRemoteDescription(new RTCSessionDescription(data));
      const answer = await pc.createAnswer();
      await pc.setLocalDescription(answer);
      state.voice.ws.send(JSON.stringify({
        type: "signal",
        to_user_id: fromPeerId,
        kind: "answer",
        data: pc.localDescription,
      }));
    } else if (kind === "answer") {
      await pc.setRemoteDescription(new RTCSessionDescription(data));
    } else if (kind === "ice") {
      try {
        await pc.addIceCandidate(data ? new RTCIceCandidate(data) : null);
      } catch (err) {
        if (!pc.ignoreOffer) {
          console.warn("failed to add ice candidate", err);
        }
      }
    }
  } catch (err) {
    console.error("handleSignal failed", err);
  }
}

async function joinVoice() {
  if (!state.selectedVoiceChannelId) {
    alert('No voice channel selected.');
    return;
  }
  if (state.voice.joinedChannelId === state.selectedVoiceChannelId &&
    state.voice.ws &&
    state.voice.ws.readyState === WebSocket.OPEN) {
    return;
  }

  await leaveVoice();

  el.voiceConnection.classList.remove('hidden');
  el.vcChannelName.textContent = 'Connecting...';

  const ws = new WebSocket(getVoiceWsUrl(state.selectedVoiceChannelId));
  const currentWs = ws;
  let wsOpened = false;
  let micUnavailable = false;

  state.voice.ws = ws;
  state.voice.joinedChannelId = state.selectedVoiceChannelId;
  storageSet("active_voice_channel", state.selectedVoiceChannelId);

  // Start mic setup in parallel so channel join is not blocked by device init.
  createLocalVoiceStream()
    .then(async () => {
      if (state.voice.ws !== currentWs) {
        stopAndClearAudioPipeline();
        return;
      }
      try {
        await attachLocalAudioToPeerConnections();
      } catch (err) {
        console.warn('failed to attach local audio to existing peer connections', err);
      }
    })
    .catch((err) => {
      micUnavailable = true;
      console.warn('failed to initialize local voice stream, joining as listen-only', err);
      state.voice.localStream = null;
      state.voice.rawStream = null;
      if (state.voice.audioContext) {
        state.voice.audioContext.close().catch(() => { });
        state.voice.audioContext = null;
      }
    });

  const connectTimer = setTimeout(() => {
    if (!wsOpened && state.voice.ws === currentWs) {
      console.error('voice websocket connect timeout');
      try { currentWs.close(); } catch (_) { }
      el.vcChannelName.textContent = 'Connection timeout';
      alert('Could not connect to voice channel (timeout). Please try again.');
    }
  }, 5000);

  ws.onopen = () => {
    if (state.voice.ws !== currentWs) return;
    wsOpened = true;
    clearTimeout(connectTimer);
    const channel = state.channels.find(c => c.id === state.selectedVoiceChannelId);
    el.vcChannelName.textContent = channel ? channel.name : "Voice";
    if (micUnavailable) {
      console.warn('Voice joined in listen-only mode because microphone is unavailable.');
    }
    el.voiceConnection.classList.remove("hidden");
    el.soundboard.classList.remove("hidden");
    playSound('join');
    refreshVoicePresence().catch(() => { });
    loadSounds().catch(() => { });
  };

  ws.onmessage = async (event) => {
    if (state.voice.ws !== currentWs) return;
    const msg = JSON.parse(event.data);
    if (msg.type === "peers") {
      for (const peer of msg.peers) {
        ensurePeerConnection(peer.user_id);
      }
    } else if (msg.type === "peer_joined") {
      playSound('peer-join');
      ensurePeerConnection(msg.user_id);
    } else if (msg.type === "peer_left") {
      playSound('peer-leave');
      const pc = state.voice.peerConnections.get(msg.user_id);
      if (pc) {
        pc.close();
        state.voice.peerConnections.delete(msg.user_id);
      }
      document.getElementById(`audio-${msg.user_id}`)?.remove();
    } else if (msg.type === "signal") {
      await handleSignal(msg.from_user_id, msg.kind, msg.data);
    } else if (msg.type === "video_status_changed") {
      if (!msg.is_sharing_video) {
        document.getElementById(`video-${msg.user_id}-camera`)?.parentElement?.remove();
      }
    } else if (msg.type === "screen_status_changed") {
      if (!msg.is_sharing_screen) {
        document.getElementById(`video-${msg.user_id}-screen`)?.parentElement?.remove();
      }
    } else if (msg.type === "speaking_status_changed") {
      state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => {
        if (p.user_id === msg.user_id) p.is_speaking = msg.is_speaking;
      });
      renderChannels();
      const videoEl = document.getElementById(`video-${msg.user_id}-camera`) || document.getElementById(`video-${msg.user_id}-screen`);
      if (videoEl) {
        videoEl.parentElement.classList.toggle('speaking', msg.is_speaking);
      }
    } else if (msg.type === "mute_status_changed") {
      state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => {
        if (p.user_id === msg.user_id) p.is_muted = msg.is_muted;
      });
      renderChannels();
    } else if (msg.type === "play_sound") {
      const audio = new Audio(msg.sound_url);
      audio.play().catch(console.error);
    }
    refreshVoicePresence().catch(() => { });
  };

  ws.onerror = (event) => {
    if (state.voice.ws !== currentWs) return;
    console.error('voice websocket error', event);
    el.vcChannelName.textContent = 'Connection failed';
  };

  ws.onclose = (event) => {
    if (state.voice.ws !== currentWs) return;
    clearTimeout(connectTimer);
    if (!wsOpened) {
      console.error('voice websocket closed before open', event.code, event.reason);
      const code = event && typeof event.code === 'number' ? event.code : 'unknown';
      el.vcChannelName.textContent = `Connection failed (${code})`;
      alert(`Voice connection failed (code ${code}).`);
    }
    if (wsOpened) {
      el.voiceConnection.classList.add("hidden");
    }
    el.soundboard.classList.add("hidden");
    for (const pc of state.voice.peerConnections.values()) pc.close();
    state.voice.peerConnections.clear();
    state.voice.videoSenders.clear();
    state.voice.screenSenders.clear();
    stopAndClearAudioPipeline();
    state.voice.joinedChannelId = null;
    state.voice.ws = null;
    playSound('leave');
    refreshVoicePresence().catch(() => { });
  };
}

async function leaveVoice() {
  storageRemove("active_voice_channel");
  if (state.voice.ws) state.voice.ws.close();
}

function toggleMute() {
  if (!state.voice.localStream) return;
  state.voice.muted = !state.voice.muted;
  state.voice.localStream.getAudioTracks().forEach((t) => {
    t.enabled = !state.voice.muted;
  });
  if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) {
    state.voice.ws.send(JSON.stringify({ type: "set_mute_status", is_muted: state.voice.muted }));
  }
  el.voiceMuteBtn.innerHTML = state.voice.muted ? '<i data-lucide="mic-off"></i>' : '<i data-lucide="mic"></i>';
  el.voiceMuteBtn.style.color = state.voice.muted ? 'var(--danger)' : 'var(--text-muted)';
  lucide.createIcons();
}

async function toggleVideo() {
  if (state.voice.sharingVideo) {
    stopAndClearVideoPipeline();
    if (state.voice.ws) {
      state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: false }));
    }
    for (const [peerId, pc] of state.voice.peerConnections) {
      const sender = state.voice.videoSenders.get(peerId);
      if (sender) {
        pc.removeTrack(sender);
        state.voice.videoSenders.delete(peerId);
      }
    }
  } else {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true });
      state.voice.videoStream = stream;
      state.voice.sharingVideo = true;
      renderVideo(state.me.id, state.me.display_name, stream, 'camera');

      if (state.voice.ws) {
        state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: true }));
      }

      for (const [peerId, pc] of state.voice.peerConnections) {
        stream.getVideoTracks().forEach(track => {
          const sender = pc.addTrack(track, stream);
          state.voice.videoSenders.set(peerId, sender);
        });
      }
    } catch (err) {
      console.error("camera denied", err);
      alert("Could not access camera.");
      return;
    }
  }

  el.voiceVideoBtn.innerHTML = state.voice.sharingVideo ? '<i data-lucide="video"></i>' : '<i data-lucide="video-off"></i>';
  el.voiceVideoBtn.style.color = state.voice.sharingVideo ? 'var(--green)' : 'var(--text-muted)';
  lucide.createIcons();
}

async function toggleScreenShare() {
  if (state.voice.sharingScreen) {
    stopAndClearScreenPipeline();
    if (state.voice.ws) {
      state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: false }));
    }
    for (const [peerId, pc] of state.voice.peerConnections) {
      const sender = state.voice.screenSenders.get(peerId);
      if (sender) {
        pc.removeTrack(sender);
        state.voice.screenSenders.delete(peerId);
      }
    }
  } else {
    try {
      const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
      state.voice.screenStream = stream;
      state.voice.sharingScreen = true;
      renderVideo(state.me.id, state.me.display_name, stream, 'screen');

      // Stop sharing if user clicks "Stop sharing" in browser UI
      stream.getVideoTracks()[0].onended = () => {
        if (state.voice.sharingScreen) toggleScreenShare();
      };

      if (state.voice.ws) {
        state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: true }));
      }

      for (const [peerId, pc] of state.voice.peerConnections) {
        stream.getVideoTracks().forEach(track => {
          const sender = pc.addTrack(track, stream);
          state.voice.screenSenders.set(peerId, sender);
        });
      }
    } catch (err) {
      console.error("screen share denied", err);
      return;
    }
  }

  el.voiceScreenBtn.innerHTML = state.voice.sharingScreen ? '<i data-lucide="monitor"></i>' : '<i data-lucide="monitor-off"></i>';
  el.voiceScreenBtn.style.color = state.voice.sharingScreen ? 'var(--green)' : 'var(--text-muted)';
  lucide.createIcons();
}

function toggleWatchVideo() {
  state.voice.watchingVideo = !state.voice.watchingVideo;
  updateVideoGridVisibility();

  el.voiceWatchBtn.innerHTML = state.voice.watchingVideo ? '<i data-lucide="eye"></i>' : '<i data-lucide="eye-off"></i>';
  el.voiceWatchBtn.style.color = state.voice.watchingVideo ? 'var(--green)' : 'var(--danger)';
  el.voiceWatchBtn.title = state.voice.watchingVideo ? 'Stop Watching' : 'Start Watching';
  lucide.createIcons();
}

// --- Sound Board ---

async function loadSounds() {
  if (!state.selectedGuildId) return;
  try {
    const sounds = await api(`/guilds/${state.selectedGuildId}/sounds`);
    renderSounds(sounds);
  } catch (err) {
    console.error("failed to load sounds", err);
  }
}

function renderSounds(sounds) {
  el.soundboardGrid.innerHTML = '';
  sounds.forEach(sound => {
    const item = document.createElement('div');
    item.className = 'sound-item';

    // Only show delete button if user is creator or owner
    const canDelete = state.me && (state.me.id === sound.created_by_user_id || (state.guilds.find(g => g.id === state.selectedGuildId)?.owner_user_id === state.me.id));

    item.innerHTML = `
      <div class="sound-icon">${sound.icon}</div>
      <div class="sound-name">${sound.name}</div>
      ${canDelete ? `<button class="sound-delete" title="Delete sound"><i data-lucide="x"></i></button>` : ''}
    `;

    item.onclick = (e) => {
      if (e.target.closest('.sound-delete')) {
        e.stopPropagation();
        if (confirm(`Are you sure you want to delete "${sound.name}"?`)) {
          deleteSound(sound.id);
        }
        return;
      }
      playRemoteSound(sound.file_path);
    };

    el.soundboardGrid.appendChild(item);
  });
  lucide.createIcons();
}

async function deleteSound(soundId) {
  if (!state.selectedGuildId) return;
  try {
    const url = API_BASE_URL ? `${API_BASE_URL}/guilds/${state.selectedGuildId}/sounds/${soundId}` : `/guilds/${state.selectedGuildId}/sounds/${soundId}`;
    const response = await fetch(url, {
      method: 'DELETE'
    });
    if (!response.ok) {
      const err = await response.json();
      throw new Error(err.error || 'Delete failed');
    }
    await loadSounds();
  } catch (err) {
    alert(err.message);
  }
}

function playRemoteSound(url) {
  const fullUrl = API_BASE_URL && !url.startsWith('http') ? `${API_BASE_URL}${url}` : url;
  if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) {
    state.voice.ws.send(JSON.stringify({ type: 'play_sound', sound_url: fullUrl }));
  }
  // Also play locally immediately
  const audio = new Audio(fullUrl);
  audio.play().catch(console.error);
}

// --- Mobile Logic ---

function toggleMobileMenu() {
  const isOpen = el.main.classList.contains("menu-open");
  if (isOpen) {
    el.main.classList.remove("menu-open");
    el.overlay.classList.add("hidden");
  } else {
    el.main.classList.add("menu-open");
    el.utilitySidebar.classList.remove("active"); // close members if open
    el.overlay.classList.remove("hidden");
  }
}

function toggleMobileMembers() {
  const isOpen = el.utilitySidebar.classList.contains("active");
  if (isOpen) {
    el.utilitySidebar.classList.remove("active");
    el.overlay.classList.add("hidden");
  } else {
    el.utilitySidebar.classList.add("active");
    el.main.classList.remove("menu-open"); // close menu if open
    el.overlay.classList.remove("hidden");
  }
}

function closeMobileMenus() {
  el.main.classList.remove("menu-open");
  el.utilitySidebar.classList.remove("active");
  el.overlay.classList.add("hidden");
}

// --- Initialization ---

let inactivityTimer = null;
let isCurrentlyIdle = false;
let updaterInitialized = false;

function resetInactivityTimer() {
  if (isCurrentlyIdle) {
    isCurrentlyIdle = false;
    if (state.chatWs && state.chatWs.readyState === WebSocket.OPEN) {
      state.chatWs.send(JSON.stringify({ type: "set_idle_status", is_idle: false }));
    }
  }

  if (inactivityTimer) clearTimeout(inactivityTimer);
  inactivityTimer = setTimeout(() => {
    isCurrentlyIdle = true;
    if (state.chatWs && state.chatWs.readyState === WebSocket.OPEN) {
      state.chatWs.send(JSON.stringify({ type: "set_idle_status", is_idle: true }));
    }
  }, 5 * 60 * 1000); // 5 minutes
}

document.addEventListener('mousemove', resetInactivityTimer);
document.addEventListener('keydown', resetInactivityTimer);
document.addEventListener('click', resetInactivityTimer);

async function init() {
  lucide.createIcons();
  await hydrateDesktopStorage();
  initUpdater();

  // 1. Handle tokens in URL (from successful login redirects)
  try {
    const searchParams = new URLSearchParams(location.search);
    const jwtToken = searchParams.get("token");
    const refreshToken = searchParams.get("refresh_token");

    if (jwtToken || refreshToken) {
      if (jwtToken) await storageSetCritical("chattz_token", jwtToken);
      if (refreshToken) await storageSetCritical("chattz_refresh_token", refreshToken);

      searchParams.delete("token");
      searchParams.delete("refresh_token");
      const nextQuery = searchParams.toString();
      const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`;
      history.replaceState(null, "", nextUrl);
    }
  } catch (e) {
    console.error("Failed to parse token from URL:", e);
  }

  // 2. Global Button Handlers
  el.loginBtn.onclick = () => {
    const loginPath = "/auth/login";
    location.href = API_BASE_URL ? `${API_BASE_URL}${loginPath}` : loginPath;
  };

  el.logoutBtn.onclick = async () => {
    await leaveVoice();
    await storageRemoveCritical("chattz_token");
    await storageRemoveCritical("chattz_refresh_token");
    try { await api("/auth/logout", { method: "POST" }); } catch (e) { }
    location.reload();
  };

  el.addGuildBtn.onclick = () => { el.modalContainer.classList.remove("hidden"); };
  el.createInviteBtn.onclick = createInviteLink;
  el.modalCancel.onclick = () => { el.modalContainer.classList.add("hidden"); };

  el.addTextBtn.onclick = () => {
    if (!state.selectedGuildId) { alert("Create or select a server first."); return; }
    document.querySelector('input[name="channel-kind"][value="text"]').checked = true;
    el.channelModal.classList.remove("hidden");
  };
  el.addVoiceBtn.onclick = () => {
    if (!state.selectedGuildId) { alert("Create or select a server first."); return; }
    document.querySelector('input[name="channel-kind"][value="voice"]').checked = true;
    el.channelModal.classList.remove("hidden");
  };
  el.channelModalCancel.onclick = () => { el.channelModal.classList.add("hidden"); };

  // Mobile Listeners
  if (el.mobileMenuBtn) el.mobileMenuBtn.onclick = toggleMobileMenu;
  if (el.mobileMembersBtn) el.mobileMembersBtn.onclick = toggleMobileMembers;
  if (el.overlay) el.overlay.onclick = closeMobileMenus;

  // 3. Form Submissions
  el.guildForm.onsubmit = async (e) => {
    e.preventDefault();
    try {
      const guild = await api("/guilds", { method: "POST", body: JSON.stringify({ name: el.guildName.value }) });
      el.guildName.value = "";
      el.modalContainer.classList.add("hidden");
      state.guilds.push(guild);
      state.selectedGuildId = guild.id;
      state.selectedTextChannelId = null;
      state.selectedVoiceChannelId = null;
      state.selectedDmUserId = null;
      state.selectedDmDisplayName = null;
      storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
      renderGuilds();
      await loadChannels();
      await loadGuildMembers();
      await refreshVoicePresence();
      renderMessages([]);
      updateHeaderLabels();
    } catch (err) { alert(err.message); }
  };

  el.channelForm.onsubmit = async (e) => {
    e.preventDefault();
    if (!state.selectedGuildId) { alert("Select a server first."); return; }
    const kindInput = document.querySelector('input[name="channel-kind"]:checked');
    const kind = kindInput ? kindInput.value : "text";
    const channelName = el.channelName.value.trim();
    if (!channelName) { alert("Channel name is required."); return; }
    try {
      const created = await api("/channels", {
        method: "POST",
        body: JSON.stringify({ guild_id: state.selectedGuildId, name: channelName, kind: kind }),
      });
      el.channelName.value = "";
      el.channelModal.classList.add("hidden");
      await loadChannels();
      if (created.kind === "text") {
        state.selectedTextChannelId = created.id;
        state.selectedVoiceChannelId = null;
        state.selectedDmUserId = null;
        state.selectedDmDisplayName = null;
        renderChannels();
        renderDMs();
        updateHeaderLabels();
        const messages = await api(`/channels/${created.id}/messages?limit=100`);
        renderMessages(messages);
      } else {
        state.selectedVoiceChannelId = created.id;
        state.selectedTextChannelId = null;
        state.selectedDmUserId = null;
        state.selectedDmDisplayName = null;
        renderChannels();
        renderDMs();
        updateHeaderLabels();
        await joinVoice();
      }
    } catch (err) { alert(err.message); }
  };

  el.messageForm.onsubmit = async (e) => {
    e.preventDefault();
    const body = el.messageBody.value.trim();
    if (!body) return;
    try {
      if (state.selectedTextChannelId) {
        await api(`/channels/${state.selectedTextChannelId}/messages`, { method: "POST", body: JSON.stringify({ body }) });
      } else if (state.selectedDmUserId) {
        await api(`/dms/${state.selectedDmUserId}/messages`, { method: "POST", body: JSON.stringify({ body }) });
      } else return;
      el.messageBody.value = "";
      const messages = state.selectedTextChannelId
        ? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`)
        : await api(`/dms/${state.selectedDmUserId}/messages?limit=100`);
      renderMessages(messages);
      await loadDMConversations();
      renderDMs();
    } catch (err) { alert(err.message); }
  };

  if (el.mediaUploadBtn && el.mediaFileInput) {
    el.mediaUploadBtn.onclick = () => {
      if (!state.selectedTextChannelId && !state.selectedDmUserId) {
        alert("Select a chat first.");
        return;
      }
      el.mediaFileInput.click();
    };

    el.mediaFileInput.onchange = async () => {
      const file = el.mediaFileInput.files?.[0];
      if (!file) return;
      try {
        await uploadMediaFile(file);
      } catch (err) {
        alert(err.message);
      } finally {
        el.mediaFileInput.value = "";
      }
    };
  }

  // 4. Voice/Soundboard Controls
  el.voiceVideoBtn.onclick = toggleVideo;
  el.voiceScreenBtn.onclick = toggleScreenShare;
  el.voiceMuteBtn.onclick = toggleMute;
  el.voiceLeaveBtn.onclick = leaveVoice;

  el.addSoundBtn.onclick = () => { el.soundModal.classList.remove("hidden"); };
  el.soundModalCancel.onclick = () => { el.soundModal.classList.add("hidden"); };

  el.soundForm.onsubmit = async (e) => {
    e.preventDefault();
    if (!state.selectedGuildId) return;
    const formData = new FormData();
    formData.append('name', el.soundName.value);
    formData.append('icon', el.soundIcon.value);
    formData.append('file', el.soundFile.files[0]);
    el.soundSubmitBtn.disabled = true;
    el.soundSubmitBtn.textContent = 'Uploading...';
    try {
      const url = API_BASE_URL ? `${API_BASE_URL}/guilds/${state.selectedGuildId}/sounds` : `/guilds/${state.selectedGuildId}/sounds`;
      const response = await fetch(url, { method: 'POST', body: formData });
      if (!response.ok) { const err = await response.json(); throw new Error(err.error || 'Upload failed'); }
      el.soundModal.classList.add("hidden");
      el.soundForm.reset();
      await loadSounds();
    } catch (err) { alert(err.message); } finally {
      el.soundSubmitBtn.disabled = false;
      el.soundSubmitBtn.textContent = 'Add Sound';
    }
  };

  // 5. GIF Picker
  const TENOR_API_KEY = "exTiFGKJ0CzESIHzVQWy3pRO8I1MAdpRomg95DBSu2sg6e7YcHgThMI4giGAx8D0";
  const TENOR_CLIENT_KEY = "pavel-discord";

  el.gifBtn.onclick = () => { el.gifPicker.classList.remove("hidden"); searchGifs(""); };
  el.gifPickerClose.onclick = () => { el.gifPicker.classList.add("hidden"); };

  let gifSearchTimeout = null;
  el.gifSearchInput.oninput = () => {
    clearTimeout(gifSearchTimeout);
    gifSearchTimeout = setTimeout(() => { searchGifs(el.gifSearchInput.value); }, 500);
  };

  async function searchGifs(query) {
    el.gifResults.innerHTML = '<div class="gif-loading">Searching...</div>';
    const baseUrl = "https://api.klipy.com/v2";
    const endpoint = query
      ? `${baseUrl}/search?q=${encodeURIComponent(query)}&key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=20`
      : `${baseUrl}/featured?key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=20`;
    try {
      const res = await fetch(endpoint);
      const data = await res.json();
      renderGifs(data.results);
    } catch (err) { el.gifResults.innerHTML = '<div class="gif-loading">Error loading GIFs</div>'; }
  }

  function renderGifs(gifs) {
    el.gifResults.innerHTML = "";
    if (!gifs || gifs.length === 0) { el.gifResults.innerHTML = '<div class="gif-loading">No GIFs found</div>'; return; }
    gifs.forEach(gif => {
      const previewUrl = gif.media_formats.tinygif.url;
      const fullUrl = gif.media_formats.gif.url;
      const item = document.createElement("div");
      item.className = "gif-item";
      const img = document.createElement("img");
      img.src = previewUrl;
      img.loading = "lazy";
      img.onclick = () => {
        el.gifPicker.classList.add("hidden");
        el.gifSearchInput.value = "";
        sendGif(fullUrl);
      };
      item.appendChild(img);
      el.gifResults.appendChild(item);
    });
  }

  async function sendGif(url) {
    try {
      if (state.selectedTextChannelId) {
        await api(`/channels/${state.selectedTextChannelId}/messages`, {
          method: "POST",
          body: JSON.stringify({ body: url }),
        });
      } else if (state.selectedDmUserId) {
        await api(`/dms/${state.selectedDmUserId}/messages`, {
          method: "POST",
          body: JSON.stringify({ body: url }),
        });
      }
      const messages = state.selectedTextChannelId
        ? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`)
        : await api(`/dms/${state.selectedDmUserId}/messages?limit=100`);
      renderMessages(messages);
    } catch (err) {
      console.error("Failed to send GIF", err);
    }
  }

  async function uploadMediaFile(file) {
    const token = storageGet("chattz_token");
    if (!token) throw new Error("You need to log in again.");

    const path = state.selectedTextChannelId
      ? `/channels/${state.selectedTextChannelId}/attachments`
      : state.selectedDmUserId
        ? `/dms/${state.selectedDmUserId}/attachments`
        : null;
    if (!path) throw new Error("Select a chat first.");

    const formData = new FormData();
    formData.append("file", file);

    el.mediaUploadBtn.disabled = true;
    el.mediaUploadBtn.title = "Uploading...";

    const fullUrl = API_BASE_URL ? `${API_BASE_URL}${path}` : path;
    try {
      const response = await fetch(fullUrl, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
        },
        body: formData,
      });

      if (!response.ok) {
        let detail = "upload failed";
        try {
          const payload = await response.json();
          detail = payload.error || detail;
        } catch { }
        throw new Error(`${response.status}: ${detail}`);
      }

      const messages = state.selectedTextChannelId
        ? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`)
        : await api(`/dms/${state.selectedDmUserId}/messages?limit=100`);
      renderMessages(messages);
      await loadDMConversations();
      renderDMs();
    } finally {
      el.mediaUploadBtn.disabled = false;
      el.mediaUploadBtn.title = "Upload File";
    }
  }

  // 6. Initial Data Loading & App Start
  try {
    state.me = await api("/me");
    if (state.me && state.me.display_name) {
      el.userName.textContent = state.me.display_name;
      el.userAvatar.textContent = shortName(state.me.display_name);
    }
    el.authScreen.classList.add("hidden");
    el.main.classList.remove("hidden");

    // Check for invite code in URL
    const params = new URLSearchParams(location.search);
    const inviteCode = params.get("invite");
    if (inviteCode) {
      try {
        const joinedGuild = await api(`/invites/${encodeURIComponent(inviteCode)}/join`, { method: "POST" });
        storageSet(LAST_GUILD_STORAGE_KEY, joinedGuild.id);
      } catch (err) { alert(`Failed to join invite: ${err.message}`); } finally {
        params.delete("invite");
        const nextQuery = params.toString();
        const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`;
        history.replaceState(null, "", nextUrl);
      }
    }

    state.guilds = await api("/guilds");
    renderGuilds();
    await loadDMConversations();

    try {
      const online = await api("/presence");
      state.onlineUsers = new Set(online);
    } catch (err) { console.warn("presence sync failed", err); }

    if (state.guilds.length > 0) {
      const lastGuildId = storageGet(LAST_GUILD_STORAGE_KEY);
      const guild = state.guilds.find((g) => g.id === lastGuildId) || state.guilds[0];
      state.selectedGuildId = guild.id;
      storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
      renderGuilds();
      await loadChannels();
      await loadGuildMembers();
      await refreshVoicePresence();
      updateHeaderLabels();
    } else {
      state.members = [];
      renderMembers();
      updateHeaderLabels();
    }

    initChatWs();
    startVoicePresencePolling();

    // Auto-reconnect to voice if previously joined
    const savedVoiceChannel = storageGet("active_voice_channel");
    if (savedVoiceChannel) {
      console.log("Auto-reconnecting to voice channel:", savedVoiceChannel);
      state.selectedVoiceChannelId = savedVoiceChannel;
      setTimeout(() => {
        joinVoice().catch(err => console.error("Auto-reconnect failed", err));
      }, 800);
    }

    lucide.createIcons();
  } catch (err) {
    console.error("init failed", err);
    el.authScreen.classList.remove("hidden");
    el.main.classList.add("hidden");
  }
}

function initUpdater() {
  if (!window.electronAPI || !el.updateNotifier || !el.updateDownloadBtn || !el.updateInstallBtn) return;
  if (updaterInitialized) return;
  updaterInitialized = true;

  const applyUpdateState = (state) => {
    if (!state || !state.status) return;
    if (state.status === 'available') {
      const v = state.info?.version ? ` ${state.info.version}` : '';
      el.updateNotifier.classList.remove("hidden");
      el.updateDownloadBtn.classList.remove("hidden");
      el.updateInstallBtn.classList.add("hidden");
      el.updateDownloadBtn.disabled = false;
      el.updateDownloadBtn.style.opacity = "";
      el.updateDownloadBtn.title = `Download Update${v}`;
      return;
    }
    if (state.status === 'downloading') {
      el.updateNotifier.classList.remove("hidden");
      el.updateDownloadBtn.classList.remove("hidden");
      el.updateInstallBtn.classList.add("hidden");
      el.updateDownloadBtn.disabled = true;
      el.updateDownloadBtn.style.opacity = "0.5";
      el.updateDownloadBtn.title = "Downloading update...";
      return;
    }
    if (state.status === 'downloaded') {
      const v = state.info?.version ? ` ${state.info.version}` : '';
      el.updateNotifier.classList.remove("hidden");
      el.updateDownloadBtn.classList.add("hidden");
      el.updateInstallBtn.classList.remove("hidden");
      el.updateInstallBtn.title = `Install Update${v}`;
      return;
    }
    if (state.status === 'not-available') {
      el.updateNotifier.classList.add("hidden");
      el.updateDownloadBtn.disabled = false;
      el.updateDownloadBtn.style.opacity = "";
      return;
    }
    if (state.status === 'error') {
      el.updateDownloadBtn.disabled = false;
      el.updateDownloadBtn.style.opacity = "";
    }
  };

  window.electronAPI.onUpdateState((state) => {
    applyUpdateState(state);
  });

  window.electronAPI.onUpdateAvailable((info) => {
    console.log("Update available:", info.version);
    el.updateNotifier.classList.remove("hidden");
    el.updateDownloadBtn.classList.remove("hidden");
    el.updateInstallBtn.classList.add("hidden");
  });

  window.electronAPI.onUpdateDownloaded((info) => {
    console.log("Update downloaded:", info.version);
    el.updateNotifier.classList.remove("hidden");
    el.updateDownloadBtn.classList.add("hidden");
    el.updateInstallBtn.classList.remove("hidden");
    el.updateInstallBtn.title = `Install Update ${info.version}`;
  });

  window.electronAPI.onUpdateError((err) => {
    console.error("Update error:", err);
    // Optionally hide indicator on error
    // el.updateNotifier.classList.add("hidden");
  });

  el.updateDownloadBtn.onclick = () => {
    el.updateDownloadBtn.disabled = true;
    el.updateDownloadBtn.style.opacity = "0.5";
    window.electronAPI.downloadUpdate();
  };

  el.updateInstallBtn.onclick = () => {
    window.electronAPI.quitAndInstall();
  };

  // Recover missed startup events and then run a fresh check now that listeners exist.
  window.electronAPI.getUpdateState?.()
    .then((state) => applyUpdateState(state))
    .catch((err) => console.warn("Failed to fetch initial update state", err))
    .finally(() => {
      if (window.electronAPI.checkForUpdatesNow) {
        void window.electronAPI.checkForUpdatesNow()
          .then((state) => applyUpdateState(state))
          .catch((err) => console.warn("Direct update check failed", err));
      } else {
        window.electronAPI.checkForUpdates?.();
      }
    });
}

// Config fetcher will trigger init()
initializeConfig();
