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,
    audioContext: null,
    denoiserNode: null,
    deepFilterCore: null,
    peerConnections: new Map(),
    muted: false,
    iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
  },
  voicePresencePollId: null,
};

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"),

  // 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"),

  // User Panel
  userName: document.getElementById("user-name"),
  userAvatar: document.getElementById("user-avatar"),
  logoutBtn: document.getElementById("logout-btn"),

  // Voice Connection
  voiceConnection: document.getElementById("voice-connection"),
  vcChannelName: document.getElementById("vc-channel-name"),
  voiceMuteBtn: document.getElementById("voice-mute-btn"),
  voiceLeaveBtn: document.getElementById("voice-leave-btn"),

  // 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"),

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

// --- API Helpers ---

async function api(path, options = {}) {
  const res = await fetch(path, {
    ...options,
    headers: {
      "content-type": "application/json",
      ...(options.headers || {}),
    },
    credentials: "same-origin",
  });

  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 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();
}

const DEEPFILTERNET_LIB_PATH = "/static/vendor/deepfilternet3/index.esm.js";
const NSNET2_COMPAT_SUPPRESSION = 56;

let deepFilterLibPromise = null;
const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId";

// --- 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;
      localStorage.setItem(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;
        renderChannels();
        renderDMs();
        updateHeaderLabels();
        const messages = await api(`/channels/${channel.id}/messages?limit=100`);
        renderMessages(messages);
      } else {
        state.selectedDmUserId = null;
        state.selectedDmDisplayName = null;
        state.selectedVoiceChannelId = channel.id;
        renderChannels();
        renderDMs();
        joinVoice();
      }
      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";
          pRow.style.padding = "2px 8px";
          pRow.innerHTML = `<div class="avatar" style="width:20px;height:20px;font-size:10px">${shortName(p.display_name)}</div> <span>${escapeHtml(p.display_name)}</span>`;
          pList.appendChild(pRow);
        }
        el.voiceChannelList.appendChild(pList);
      }
    }
  }
  lucide.createIcons();
}

function renderDMs() {
  el.dmList.innerHTML = "";
  for (const dm of state.dmConversations) {
    const row = document.createElement("button");
    row.className = `channel-row ${state.selectedDmUserId === dm.user_id ? "active" : ""}`;
    row.innerHTML = `<i data-lucide="message-circle"></i> <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;
      renderChannels();
      renderDMs();
      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";

    if (isGrouped) {
      row.innerHTML = `<div class="msg-content"><div class="msg-body">${escapeHtml(m.body)}</div></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>
          <div class="msg-body">${escapeHtml(m.body)}</div>
        </div>
      `;
    }
    el.messageList.appendChild(row);
    lastAuthorId = m.author_user_id;
    lastTime = mDate;
  }

  el.messageList.scrollTop = el.messageList.scrollHeight;
}

function renderMembers() {
  el.memberList.innerHTML = "";
  for (const m of state.members) {
    const row = document.createElement("div");
    row.className = "member-row";
    row.innerHTML = `
      <div class="member-avatar">${shortName(m.display_name)}</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;
      renderChannels();
      renderDMs();
      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;
  }

  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 }),
    });
    const link = `${location.origin}/?invite=${encodeURIComponent(invite.code)}`;
    try {
      await navigator.clipboard.writeText(link);
      alert(`Invite link copied:\n${link}`);
    } catch {
      prompt("Copy invite link:", link);
    }
  } catch (err) {
    alert(err.message);
  }
}

// --- 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);
}

// --- Voice ---

function getVoiceWsUrl(channelId) {
  const proto = location.protocol === "https:" ? "wss" : "ws";
  return `${proto}://${location.host}/channels/${channelId}/voice/ws`;
}

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

async function loadDeepFilterLib() {
  if (!deepFilterLibPromise) {
    deepFilterLibPromise = import(DEEPFILTERNET_LIB_PATH);
  }
  return deepFilterLibPromise;
}

async function buildDenoiserNode(audioContext) {
  if (!audioContext.audioWorklet) {
    throw new Error("AudioWorklet is not supported in this browser");
  }

  const df = await loadDeepFilterLib();
  const core = new df.DeepFilterNet3Core({
    sampleRate: 48000,
    noiseReductionLevel: NSNET2_COMPAT_SUPPRESSION,
    assetConfig: {
      cdnUrl: "/static/vendor/deepfilternet3",
    },
  });
  await core.initialize();
  const workletNode = await core.createAudioWorkletNode(audioContext);
  state.voice.deepFilterCore = core;
  return workletNode;
}

function stopAndClearAudioPipeline() {
  if (state.voice.deepFilterCore) {
    state.voice.deepFilterCore.destroy();
    state.voice.deepFilterCore = null;
  }
  if (state.voice.denoiserNode && typeof state.voice.denoiserNode.destroy === "function") {
    state.voice.denoiserNode.destroy();
  }
  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;
  state.voice.denoiserNode = null;
  state.voice.deepFilterCore = null;
}

async function buildAudioPipeline(rawStream) {
  const audioContext = new AudioContext();
  const source = audioContext.createMediaStreamSource(rawStream);
  const destination = audioContext.createMediaStreamDestination();
  state.voice.audioContext = audioContext;

  let head = source;

  const denoiserNode = await buildDenoiserNode(audioContext);
  if (denoiserNode) {
    head.connect(denoiserNode);
    head = denoiserNode;
  }
  head.connect(destination);

  state.voice.denoiserNode = denoiserNode;
  return destination.stream;
}

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);
  const localStream = await buildAudioPipeline(rawStream);
  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 });

  if (state.voice.localStream) {
    for (const track of state.voice.localStream.getTracks()) {
      pc.addTrack(track, state.voice.localStream);
    }
  }

  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.ontrack = (event) => {
    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.srcObject = event.streams[0];
  };

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

async function sendOffer(peerId) {
  const pc = ensurePeerConnection(peerId);
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  state.voice.ws.send(JSON.stringify({
    type: "signal",
    to_user_id: peerId,
    kind: "offer",
    data: offer,
  }));
}

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

  if (kind === "offer") {
    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: answer,
    }));
  } 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) {
      console.warn("failed to add ice candidate", err);
    }
  }
}

async function joinVoice() {
  if (!state.selectedVoiceChannelId) return;
  if (state.voice.joinedChannelId === state.selectedVoiceChannelId) return;

  await leaveVoice();

  try {
    await createLocalVoiceStream();
  } catch (err) {
    console.error("microphone denied", err);
    return;
  }

  const ws = new WebSocket(getVoiceWsUrl(state.selectedVoiceChannelId));
  state.voice.ws = ws;
  state.voice.joinedChannelId = state.selectedVoiceChannelId;

  ws.onopen = () => {
    const channel = state.channels.find(c => c.id === state.selectedVoiceChannelId);
    el.vcChannelName.textContent = channel ? channel.name : "Voice";
    el.voiceConnection.classList.remove("hidden");
    refreshVoicePresence().catch(() => { });
  };

  ws.onmessage = async (event) => {
    const msg = JSON.parse(event.data);
    if (msg.type === "peers") {
      for (const peer of msg.peers) {
        if (shouldInitiateOffer(peer.user_id)) await sendOffer(peer.user_id);
      }
    } else if (msg.type === "peer_joined") {
      if (shouldInitiateOffer(msg.user_id)) await sendOffer(msg.user_id);
    } else if (msg.type === "peer_left") {
      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);
    }
    refreshVoicePresence().catch(() => { });
  };

  ws.onclose = () => {
    el.voiceConnection.classList.add("hidden");
    for (const pc of state.voice.peerConnections.values()) pc.close();
    state.voice.peerConnections.clear();
    stopAndClearAudioPipeline();
    state.voice.joinedChannelId = null;
    state.voice.ws = null;
    refreshVoicePresence().catch(() => { });
  };
}

async function leaveVoice() {
  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;
  });
  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();
}

// --- 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 ---

async function init() {
  lucide.createIcons();

  el.loginBtn.onclick = () => { location.href = "/auth/login"; };

  el.logoutBtn.onclick = async () => {
    await leaveVoice();
    await api("/auth/logout", { method: "POST" });
    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;

  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;
      localStorage.setItem(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();
      }
    } 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); }
  };

  el.voiceMuteBtn.onclick = toggleMute;
  el.voiceLeaveBtn.onclick = leaveVoice;

  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");

    const params = new URLSearchParams(location.search);
    const inviteCode = params.get("invite");
    if (inviteCode) {
      try {
        const joinedGuild = await api(`/invites/${encodeURIComponent(inviteCode)}/join`, {
          method: "POST",
        });
        localStorage.setItem(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);
      }
    }

    await loadGuilds();
    await loadDMConversations();

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

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

init();
