const GAME_ID = "demo";
const DEFAULT_MAP_CENTER = [48.4284, -123.3656];
const DEFAULT_MAP_ZOOM = 15;

const DEFAULT_AVATAR_PACK_ID = "free_3d_avatars_001";
const DEFAULT_AVATAR_PACK_URL = "https://dungeonwake.com/wp-content/uploads/2026/06/avatars.png";
const DEFAULT_AVATAR_COLUMNS = 10;
const DEFAULT_AVATAR_ROWS = 8;
const DEFAULT_AVATAR_COUNT = 80;

let currentGame = null;
let currentScoreboard = null;
let currentServerConfig = null;
let currentSyncSettings = null;
let currentEntitlements = null;
let currentPlatformStatus = null;
let availableGameModes = [];

let savedZones = [];
let lastUsedSavedZoneId = "";

let adminMap = null;
let zoneLayer = null;
let objectiveLayer = null;
let draftLayer = null;
let zoneDraftPolygon = null;
let objectiveDraftMarker = null;
let zoneDrawPoints = [];
let zoneVertexMarkers = [];
let mapClickMode = "none";
let currentZone = null;
let activeAdminTab = "gameSettings";
let selectedProfileAvatarIndex = -1;
let editingTeamId = "";
let objectiveMoveInProgress = false;
let syncSettingsDirty = false;
let suppressSyncSettingsRenderUntil = 0;
let adminStaticDataLoaded = false;
let adminFormDirtyUntil = 0;
let lastObservedAdminGameStatus = "";
let startGameRequestInFlight = false;
let suppressAutoScoreboardRedirectUntil = 0;
const ADMIN_BACKGROUND_REFRESH_MS = 1000;
// Local-only admin refresh. This does not touch Firestore directly; it keeps the
// desktop join lobby responsive after rematch/rejoin flows.
const ADMIN_EDIT_SUPPRESS_REFRESH_MS = 30000;
const CTF_MODE_ID = "capture_the_flag";
const ZOMBIE_EXTRACTION_MODE_ID = "zombie_extraction";
const ZOMBIE_TAG_MODE_ID = "zombie_tag";
const CTF_FLAG_TYPE = "ctf_flag";
const CTF_BASE_TYPE = "ctf_base";
const DEFAULT_TEAM_COLORS = ["#ff4c4c", "#45a3ff", "#ffbd62", "#5cd6c9"];
const DLC_THEME_IDS = ["haunted"];
const DLC_THEME_LABELS = {
  haunted: "Haunted"
};



function isTeamLobbyMode(modeKey) {
  return modeKey === "team_elimination" || modeKey === CTF_MODE_ID;
}

function getModeKeyForCurrentGame() {
  return canonicalGameModeKey(currentGame?.gameMode || currentGame?.mode || currentGame?.gameModeId);
}

function showAdminConfirm({ title, message, confirmText = "Confirm", cancelText = "Cancel", danger = false } = {}) {
  return new Promise(resolve => {
    const existing = document.getElementById("adminConfirmModal");
    existing?.remove();

    const modal = document.createElement("div");
    modal.id = "adminConfirmModal";
    modal.className = "admin-confirm-modal";
    modal.innerHTML = `
      <div class="admin-confirm-backdrop" data-admin-confirm-cancel="true"></div>
      <section class="admin-confirm-panel card" role="dialog" aria-modal="true" aria-labelledby="adminConfirmTitle">
        <div class="admin-confirm-kicker">Host Control</div>
        <h2 id="adminConfirmTitle">${escapeHtml(title || "Are you sure?")}</h2>
        <p>${escapeHtml(message || "Confirm this action.")}</p>
        <div class="admin-confirm-actions">
          <button class="secondary" type="button" data-admin-confirm-cancel="true">${escapeHtml(cancelText)}</button>
          <button class="${danger ? "danger-button" : "primary"}" type="button" data-admin-confirm-ok="true">${escapeHtml(confirmText)}</button>
        </div>
      </section>
    `;

    const finish = value => {
      modal.remove();
      document.removeEventListener("keydown", onKeyDown);
      resolve(value);
    };

    const onKeyDown = event => {
      if (event.key === "Escape") {
        finish(false);
      }
    };

    modal.addEventListener("click", event => {
      if (event.target?.closest?.("[data-admin-confirm-ok]")) {
        finish(true);
        return;
      }

      if (event.target?.closest?.("[data-admin-confirm-cancel]")) {
        finish(false);
      }
    });

    document.addEventListener("keydown", onKeyDown);
    document.body.appendChild(modal);
    modal.querySelector("[data-admin-confirm-ok]")?.focus();
  });
}

function canonicalGameModeKey(value) {
  const raw = String(value ?? "").trim();
  if (!raw) {
    return "";
  }

  const compact = raw.toLowerCase().replace(/[^a-z0-9]/g, "");

  if (compact.includes("zombietag")) return ZOMBIE_TAG_MODE_ID;
  if (compact.includes("zombieextraction")) return ZOMBIE_EXTRACTION_MODE_ID;
  if (compact.includes("ghosthunt")) return "ghost_hunt";
  if (compact.includes("capturetheflag") || compact === "ctf") return CTF_MODE_ID;
  if (compact.includes("teamelimination")) return "team_elimination";
  if (compact.includes("objectivecapture")) return "objective_capture";
  if (compact.includes("hotspotrush")) return "hot_spot_rush";

  return raw.toLowerCase().replace(/[\s-]+/g, "_");
}

window.onAdminAuthReady = async () => {
  await updateProfileButton();
};

window.addEventListener("admin-auth-ready", () => {
  updateProfileButton();
});


window.addEventListener("DOMContentLoaded", async () => {
  wireButtons();
  wireAdminTabs();
  wireSubTabs();

  document.addEventListener("input", event => {
    markAdminFormDirty(event.target);

    if (event.target?.id === "profileAvatarUrl") {
      updateProfileAvatarPreview();
    }
  });

  document.addEventListener("change", event => {
    markAdminFormDirty(event.target);

    if (event.target?.dataset?.gameModeSettingKey === "ghostHuntPreset") {
      applyGhostHuntPresetToFields(event.target.value);
    }

    if (event.target?.dataset?.gameModeSettingKey === "zombieExtractionPreset") {
      applyZombieExtractionPresetToFields(event.target.value);
    }
  });

  document.addEventListener("focusin", event => {
    markAdminFormDirty(event.target);
  });

  document.getElementById("gameModeSelect")?.addEventListener("change", () => {
    setGameMode();
  });

  document.getElementById("themeSelect")?.addEventListener("change", event => {
    handleThemeSelectionChanged(event.target);
  });

  document.getElementById("gameDurationMinutes")?.addEventListener("change", () => {
    saveGameSettings();
  });

  document.getElementById("maxPlayers")?.addEventListener("change", () => {
    clampMaxPlayersToEntitlement();
    saveGameSettings();
  });

  document.getElementById("allowProximityTagging")?.addEventListener("change", () => {
    saveGameSettings();
  });

  document.getElementById("proximityTagHoldSeconds")?.addEventListener("change", () => {
    clampProximityTagHoldSeconds();
    saveGameSettings();
  });

  document.getElementById("saveGameModeSettingsButton")?.addEventListener("click", () => {
    saveGameSettings();
  });

  [
    "syncIntervalSeconds",
    "locationUploadSeconds",
    "locationMinDistanceMeters",
    "maxLocationAgeSeconds",
    "enableRealtimeCriticalUpdates"
  ].forEach(id => {
    const el = document.getElementById(id);

    if (!el) {
      return;
    }

    el.addEventListener("input", markSyncSettingsDirty);
    el.addEventListener("change", markSyncSettingsDirty);
    el.addEventListener("focus", markSyncSettingsDirty);
  });

  const locationUploadInput = document.getElementById("locationUploadSeconds");
  if (locationUploadInput) {
    locationUploadInput.addEventListener("change", () => clampLocationUploadSecondsToEntitlement({ showError: true }));
    locationUploadInput.addEventListener("blur", () => clampLocationUploadSecondsToEntitlement({ showError: true }));
  }


  document.addEventListener("click", event => {
    const editTeamButton = event.target?.closest?.("[data-edit-team-id]");
    const saveTeamButton = event.target?.closest?.("[data-save-team-id]");
    const cancelTeamButton = event.target?.closest?.("[data-cancel-team-edit]");

    if (editTeamButton) {
      event.preventDefault();
      startEditTeam(editTeamButton.dataset.editTeamId);
      return;
    }

    if (saveTeamButton) {
      event.preventDefault();
      saveTeamEdit(saveTeamButton.dataset.saveTeamId);
      return;
    }

    if (cancelTeamButton) {
      event.preventDefault();
      cancelTeamEdit();
    }
  });


  setMapStatus("Loading map engine...");
  const mapEngineLoaded = await loadLeafletEngine();

  if (mapEngineLoaded) {
    initMap();
  } else {
    showMapUnavailable("The map engine did not load. This is usually a CDN/network/ad-block issue. Try a hard refresh, a different browser, or temporarily disabling browser privacy extensions.");
  }

  await loadAdminData();
  setInterval(refreshLiveAdminDataQuietly, ADMIN_BACKGROUND_REFRESH_MS);
});

async function loadLeafletEngine() {
  if (window.L) return true;

  const sources = [
    {
      css: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css",
      js: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"
    },
    {
      css: "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css",
      js: "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"
    },
    {
      css: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",
      js: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
    }
  ];

  for (const source of sources) {
    try {
      setMapStatus(`Trying map engine: ${new URL(source.js).hostname}...`);
      await loadCss(source.css);
      await loadScript(source.js);
      if (window.L) {
        setMapStatus("Map engine loaded.");
        return true;
      }
    } catch {
      // Try next CDN.
    }
  }

  return false;
}

function loadCss(href) {
  return new Promise(resolve => {
    if ([...document.styleSheets].some(sheet => sheet.href === href)) {
      resolve();
      return;
    }

    const link = document.createElement("link");
    link.rel = "stylesheet";
    link.href = href;
    link.onload = () => resolve();
    link.onerror = () => resolve();
    document.head.appendChild(link);
  });
}

function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = src;
    script.async = true;
    script.onload = () => resolve();
    script.onerror = () => reject(new Error(`Failed to load ${src}`));
    document.body.appendChild(script);
  });
}

let mapStatusHideTimer = null;

function setMapStatus(message, type = "") {
  const el = document.getElementById("adminMapStatus");
  if (!el) return;

  if (mapStatusHideTimer) {
    clearTimeout(mapStatusHideTimer);
    mapStatusHideTimer = null;
  }

  el.textContent = message;
  el.className = type ? `map-status ${type}` : "map-status";
  el.style.display = message ? "block" : "none";

  if (type === "success") {
    mapStatusHideTimer = setTimeout(() => {
      el.textContent = "";
      el.style.display = "none";
    }, 2500);
  }
}

function showMapUnavailable(message) {
  setMapStatus(message, "error");
  const mapElement = document.getElementById("adminMap");
  if (mapElement) {
    mapElement.innerHTML = `<div class="map-unavailable">${escapeHtml(message)}</div>`;
  }
  showMessage(message, "error");
}

function initMap() {
  if (!window.L) {
    showMapUnavailable("Leaflet did not load. Check your internet connection or browser extensions.");
    return;
  }

  const mapElement = document.getElementById("adminMap");
  mapElement.innerHTML = "";

  adminMap = L.map("adminMap", {
    zoomControl: true,
    preferCanvas: false
  }).setView(DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM);

  const tileLayer = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
    maxZoom: 19,
    attribution: "&copy; OpenStreetMap contributors",
    crossOrigin: true
  });

  tileLayer.on("tileerror", () => {
    setMapStatus("Map engine loaded, but map image tiles are not loading. Check internet access, browser privacy/ad-block settings, or try a different browser.", "error");
    updateMapHelp("Map tiles are not loading, but drawing tools may still work if the map engine loaded.");
  });

  tileLayer.on("load", () => {
    setMapStatus("Map loaded.", "success");
  });

  tileLayer.addTo(adminMap);

  zoneLayer = L.layerGroup().addTo(adminMap);
  objectiveLayer = L.layerGroup().addTo(adminMap);
  draftLayer = L.layerGroup().addTo(adminMap);

  window.setTimeout(() => adminMap.invalidateSize(), 150);
  window.setTimeout(() => adminMap.invalidateSize(), 700);

  adminMap.on("click", event => {
    if (mapClickMode === "zone") {
      addZoneDraftPoint(event.latlng);
      return;
    }

    if (mapClickMode === "objective") {
      setObjectiveDraftLocation(event.latlng);
      mapClickMode = "none";
      updateMapHelp("Objective location selected. Fill the objective details, then click Add Objective.");
    }
  });
}

function wireAdminTabs() {
  document.querySelectorAll("[data-admin-tab]").forEach(button => {
    button.addEventListener("click", () => {
      if (button.disabled || button.classList.contains("disabled-tab")) {
        return;
      }

      const tabName = button.dataset.adminTab;
      if (!tabName) return;

      activeAdminTab = tabName;

      document.querySelectorAll("[data-admin-tab]").forEach(tabButton => {
        tabButton.classList.toggle("active", tabButton.dataset.adminTab === tabName);
      });

      document.querySelectorAll(".admin-tab-panel").forEach(panel => {
        panel.classList.toggle("active", panel.id === `tab-${tabName}`);
      });

      if (tabName === "playZone" && adminMap) {
        window.setTimeout(() => adminMap.invalidateSize(), 80);
        window.setTimeout(() => renderMapData({ preserveMapView: true }), 100);
      }
    });
  });
}

function wireSubTabs() {
  document.querySelectorAll(".sub-tab-button").forEach(button => {
    button.addEventListener("click", event => {
      event.preventDefault();

      if (button.disabled || button.classList.contains("disabled-tab")) {
        return;
      }

      const subTabName = button.dataset.subTab;
      if (!subTabName) {
        return;
      }

      document.querySelectorAll(".sub-tab-button").forEach(item => {
        item.classList.toggle("active", item.dataset.subTab === subTabName);
      });

      document.querySelectorAll(".sub-tab-panel").forEach(panel => {
        panel.classList.toggle("active", panel.id === `sub-tab-${subTabName}`);
      });

      if (activeAdminTab === "playZone" && adminMap) {
        window.setTimeout(() => adminMap.invalidateSize(), 80);
        window.setTimeout(() => renderMapData({ preserveMapView: true }), 100);
      }
    });
  });
}

function wireButtons() {
  bindButton("startGameButton", enterJoinLobby);
  bindButton("actualStartGameButton", startGameAndOpenScoreboard);
  bindButton("cancelReadyButton", cancelReadyLobby);

  bindButton("saveZoneButton", addZone);
  bindButton("saveZonePresetButton", saveCurrentZonePreset);
  bindButton("editExistingZoneButton", editExistingZone);

  bindButton("saveServerConfigButton", saveServerConfig);
  bindButton("saveSyncSettingsButton", saveSyncSettings);

  bindButton("saveObjectiveButton", addObjective);
  bindButton("fillZoneLocationButton", () => fillCurrentLocation("zoneLat", "zoneLon", true));
  bindButton("centerMyLocationButton", centerMapOnCurrentLocation);
  bindButton("searchAddressButton", searchAddress);
  bindButton("drawBoundaryButton", startZoneDrawing);
  bindButton("clearBoundaryDraftButton", clearZoneDraft);
  bindButton("placeObjectiveButton", startObjectivePlacement);

  bindButton("profileButton", openProfileModal);
  bindButton("saveProfileButton", saveProfile);
  bindButton("logoutButton", logoutAdmin);
  bindButton("closeProfileButton", closeProfileModal);
  bindButton("closeProfileModalButton", closeProfileModal);

}

function bindButton(id, handler) {
  const button = document.getElementById(id);

  if (!button) {
    console.warn(`Button not found: ${id}`);
    return;
  }

  button.addEventListener("click", event => {
    event.preventDefault();
    handler();
  });
}

async function getEntitlementsSafe() {
  try {
    const serverConfig = await getJson(`/api/admin/server-config`);
    const ownerUserId = serverConfig.ownerUserId;

    if (!ownerUserId) {
      throw new Error("Owner User ID is missing from server config.");
    }

    const response = await getJson(`/api/cloud/users/${encodeURIComponent(ownerUserId)}/entitlement`);
    return response.entitlement || response;
  } catch {
    return {
      planId: "free",
      planName: "Free",
      maxPlayersPerGame: 8,
      maxAndroidHostPlayers: 8,
      maxWindowsHostPlayers: 12
    };
  }
}

async function loadAdminData(options = {}) {
  try {
    const [
      game,
      scoreboard,
      gameModesResponse,
      savedZonesResponse,
      serverConfig,
      syncSettings,
      entitlements
    ] = await Promise.all([
      getJson(`/api/games/${GAME_ID}`),
      getJson(`/api/games/${GAME_ID}/scoreboard`),
      getJson(`/api/game-modes`),
      getJson(`/api/saved-zones`),
      getJson(`/api/admin/server-config`),
      getJson(`/api/admin/sync-settings`),
      getEntitlementsSafe()
    ]);

    observeAdminGameStatusForRemoteStart(game, { initialLoad: true });
    currentGame = game;
    currentScoreboard = scoreboard;
    availableGameModes = gameModesResponse.gameModes || [];
    currentPlatformStatus = gameModesResponse.platformStatus || null;
    savedZones = savedZonesResponse.savedZones || [];
    lastUsedSavedZoneId = savedZonesResponse.lastUsedZone?.savedZoneId || "";

    currentServerConfig = serverConfig;
    currentSyncSettings = syncSettings;
    currentEntitlements = entitlements;
    adminStaticDataLoaded = true;

    renderServerConfig(serverConfig);
    renderSyncSettings(syncSettings);
    renderEntitlements(entitlements);

    renderAdmin({
      preserveMapView: options.preserveMapView === true || options.quiet === true
    });

    if (!options.quiet) {
      showMessage("Admin loaded.", "success", false);
    }
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function loadAdminDataQuietly() {
  // Kept as a compatibility wrapper for older button handlers.
  await refreshLiveAdminDataQuietly();
}

async function refreshLiveAdminDataQuietly() {
  if (shouldPauseBackgroundAdminRefresh()) {
    return;
  }

  try {
    const [game, scoreboard] = await Promise.all([
      getJson(`/api/games/${GAME_ID}`),
      getJson(`/api/games/${GAME_ID}/scoreboard`)
    ]);

    observeAdminGameStatusForRemoteStart(game, { initialLoad: false });
    currentGame = game;
    currentScoreboard = scoreboard;

    renderAdmin({
      preserveMapView: true,
      liveRefresh: true
    });
  } catch (error) {
    // Background refresh should not interrupt admin editing.
  }
}

function shouldPauseBackgroundAdminRefresh() {
  // Once the host is looking at the join lobby, roster freshness matters more
  // than preserving a stale settings edit debounce. Without this escape hatch,
  // editing settings before Ready could suppress the local admin refresh for
  // ADMIN_EDIT_SUPPRESS_REFRESH_MS and make newly joined players appear late
  // after cancel -> ready or rematch-style flows. This is local server polling
  // only; it does not add Firestore reads.
  if (activeAdminTab === "joinLobby") {
    return false;
  }

  if (editingTeamId || objectiveMoveInProgress || syncSettingsDirty || Date.now() < adminFormDirtyUntil) {
    return true;
  }

  return isAdminEditableElement(document.activeElement);
}

function markAdminFormDirty(target) {
  if (!isAdminEditableElement(target)) {
    return;
  }

  target.dataset.adminDirty = "true";
  adminFormDirtyUntil = Date.now() + ADMIN_EDIT_SUPPRESS_REFRESH_MS;
}

function observeAdminGameStatusForRemoteStart(game, options = {}) {
  const nextStatus = (game?.status || "").toString().toLowerCase();
  const previousStatus = lastObservedAdminGameStatus;
  lastObservedAdminGameStatus = nextStatus;

  if (options.initialLoad) {
    return;
  }

  if (Date.now() < suppressAutoScoreboardRedirectUntil) {
    return;
  }

  const transitionedToActive = nextStatus === "active" && previousStatus && previousStatus !== "active";
  if (transitionedToActive) {
    window.location.href = `/scoreboard/45D.3?ts=${Date.now()}`;
  }
}

function clearAdminFormDirty() {
  adminFormDirtyUntil = 0;

  document
    .querySelectorAll("[data-admin-dirty='true']")
    .forEach(el => {
      delete el.dataset.adminDirty;
    });
}

function isAdminEditableElement(element) {
  if (!element || !element.tagName) {
    return false;
  }

  if (element.closest?.("#profileModal")) {
    return false;
  }

  const tagName = element.tagName.toUpperCase();
  return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
}

async function openProfileModal() {
  const modal = document.getElementById("profileModal");
  if (!modal) return;

  modal.classList.remove("hidden");

  const profile = await window.adminAuth?.loadProfile?.();

  document.getElementById("profileDisplayName").value =
    profile?.displayName || "";

  selectedProfileAvatarIndex = Number.isFinite(Number(profile?.avatarIndex))
    ? Number(profile.avatarIndex)
    : -1;

  if (profile?.avatarType !== "sprite" || selectedProfileAvatarIndex < 0) {
    selectedProfileAvatarIndex = 0;
  }

  renderProfileAvatarPreview();
  renderProfileAvatarGrid();
}

function closeProfileModal() {
  document.getElementById("profileModal")?.classList.add("hidden");
}

async function saveProfile() {
  try {
    const profile = {
      displayName: document.getElementById("profileDisplayName").value.trim(),
      avatarUrl: "",
      avatarType: "sprite",
      avatarPackId: DEFAULT_AVATAR_PACK_ID,
      avatarPackUrl: DEFAULT_AVATAR_PACK_URL,
      avatarIndex: selectedProfileAvatarIndex >= 0 ? selectedProfileAvatarIndex : 0
    };

    await window.adminAuth.saveProfile(profile);

    showMessage("Profile saved.", "success");
    await updateProfileButton();
    closeProfileModal();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function logoutAdmin() {
  await window.adminAuth.logout();
  window.location.href = "/login";
}

function renderProfileAvatarPreview() {
  const preview = document.getElementById("profileAvatarCurrentPreview");
  if (!preview) return;

  const avatarIndex = selectedProfileAvatarIndex >= 0 ? selectedProfileAvatarIndex : 0;
  preview.innerHTML = renderSpriteAvatarHtml(avatarIndex, "profile-avatar-current-sprite");
}

function renderProfileAvatarGrid() {
  const grid = document.getElementById("profileAvatarGrid");
  if (!grid) return;

  grid.innerHTML = Array.from({ length: DEFAULT_AVATAR_COUNT }, (_, index) => {
    return `
      <button
        class="profile-avatar-option ${index === selectedProfileAvatarIndex ? "selected" : ""}"
        type="button"
        data-avatar-index="${index}">
        ${renderSpriteAvatarHtml(index, "profile-avatar-sprite")}
      </button>
    `;
  }).join("");

  grid.querySelectorAll("[data-avatar-index]").forEach(button => {
    button.addEventListener("click", event => {
      event.preventDefault();
      selectedProfileAvatarIndex = Number(button.dataset.avatarIndex);
      renderProfileAvatarPreview();
      renderProfileAvatarGrid();
    });
  });
}

function renderSpriteAvatarHtml(index, className = "sprite-avatar", packUrl = DEFAULT_AVATAR_PACK_URL) {
  const safeIndex = Math.max(0, Math.min(DEFAULT_AVATAR_COUNT - 1, Number(index) || 0));
  const column = safeIndex % DEFAULT_AVATAR_COLUMNS;
  const row = Math.floor(safeIndex / DEFAULT_AVATAR_COLUMNS);
  const x = DEFAULT_AVATAR_COLUMNS <= 1 ? 0 : (column / (DEFAULT_AVATAR_COLUMNS - 1)) * 100;
  const y = DEFAULT_AVATAR_ROWS <= 1 ? 0 : (row / (DEFAULT_AVATAR_ROWS - 1)) * 100;
  const safePackUrl = escapeHtml(packUrl || DEFAULT_AVATAR_PACK_URL);

  return `
    <span
      class="${className} sprite-avatar"
      style="
        background-image:url('${safePackUrl}');
        background-size:${DEFAULT_AVATAR_COLUMNS * 100}% ${DEFAULT_AVATAR_ROWS * 100}%;
        background-position:${x}% ${y}%;
      ">
    </span>
  `;
}

function renderGenericProfileIconHtml() {
  return `
    <svg viewBox="0 0 24 24" class="profile-fallback-svg">
      <path d="M12 12c2.4 0 4.3-1.9 4.3-4.3S14.4 3.4 12 3.4 7.7 5.3 7.7 7.7 9.6 12 12 12zm0 2.2c-3.2 0-6.8 1.7-6.8 4.1v1.3h13.6v-1.3c0-2.4-3.6-4.1-6.8-4.1z"></path>
    </svg>
  `;
}

function showGenericProfileButtonIcon(avatarEl, fallbackEl) {
  if (avatarEl) {
    avatarEl.removeAttribute("src");
    avatarEl.style.display = "none";
    avatarEl.classList.add("hidden");
  }

  if (fallbackEl) {
    fallbackEl.innerHTML = renderGenericProfileIconHtml();
    fallbackEl.classList.remove("hidden");
  }
}

async function updateProfileButton() {
  const avatarEl = document.getElementById("profileButtonAvatar");
  const fallbackEl = document.getElementById("profileButtonFallback");

  if (!avatarEl || !fallbackEl || !window.adminAuth?.loadProfile) {
    return;
  }

  try {
    const user = window.adminAuth?.getUser?.();
    if (!user) {
      showGenericProfileButtonIcon(avatarEl, fallbackEl);
      return;
    }

    const profile = await window.adminAuth.loadProfile();
    const avatarType = String(profile?.avatarType || "").trim().toLowerCase();
    const avatarIndex = Number(profile?.avatarIndex);
    const avatarUrl = String(profile?.avatarUrl || "").trim();
    const avatarPackUrl = String(profile?.avatarPackUrl || DEFAULT_AVATAR_PACK_URL).trim() || DEFAULT_AVATAR_PACK_URL;

    if (avatarType === "sprite" && Number.isFinite(avatarIndex) && avatarIndex >= 0) {
      avatarEl.removeAttribute("src");
      avatarEl.style.display = "none";
      avatarEl.classList.add("hidden");
      fallbackEl.innerHTML = renderSpriteAvatarHtml(avatarIndex, "profile-icon-sprite", avatarPackUrl);
      fallbackEl.classList.remove("hidden");
      return;
    }

    if (avatarUrl) {
      fallbackEl.classList.add("hidden");
      avatarEl.onload = () => {
        fallbackEl.classList.add("hidden");
        avatarEl.classList.remove("hidden");
        avatarEl.style.display = "block";
      };
      avatarEl.onerror = () => showGenericProfileButtonIcon(avatarEl, fallbackEl);
      avatarEl.src = avatarUrl;
      avatarEl.classList.remove("hidden");
      avatarEl.style.display = "block";
      return;
    }

    showGenericProfileButtonIcon(avatarEl, fallbackEl);
  } catch (error) {
    console.warn("Could not update profile button avatar.", error);
    showGenericProfileButtonIcon(avatarEl, fallbackEl);
  }
}

function renderAdmin(options = {}) {
  if (!currentGame || !currentScoreboard) return;

  renderAdminStatus();
  renderPlatformStatusPanel();
  renderReadyRequirements();

  if (!options.liveRefresh) {
    renderGameModeSelect();
    renderGameModeSettingsSection();
    renderZones();
    renderSavedZones();
  } else {
    if (!isInsideGameModeSettings(document.activeElement)) {
      renderGameModeSettingsSection();
    }
    renderZones();
  }

  renderObjectives();
  renderPlayers();
  renderTeams();
  renderJoinLobby();
  updateJoinLobbyTabState();
  renderMapData(options);

  if (!options.liveRefresh) {
    updateProfileButton();
  }
}

function isCurrentEntitlementActive() {
  const entitlement = currentEntitlements?.entitlement || currentEntitlements || {};
  if (String(entitlement.status || "active").toLowerCase() !== "active") return false;

  const subscriptionStatus = String(entitlement.subscriptionStatus || "").trim().toUpperCase();
  const blockedStatuses = new Set([
    "SUBSCRIPTION_STATE_EXPIRED",
    "SUBSCRIPTION_STATE_PAUSED",
    "SUBSCRIPTION_STATE_ON_HOLD",
    "SUBSCRIPTION_STATE_REVOKED",
    "SUBSCRIPTION_STATE_PENDING_PURCHASE_CANCELED",
    "EXPIRED",
    "PAUSED",
    "ON_HOLD",
    "REVOKED",
    "CANCELED_WITHOUT_ACCESS"
  ]);
  if (blockedStatuses.has(subscriptionStatus)) return false;

  const rawExpiry = entitlement.subscriptionExpiresAtUtc ?? entitlement.subscriptionExpiresAt;
  if (!rawExpiry) return true;
  const expiryMillis = typeof rawExpiry === "object" && Number.isFinite(Number(rawExpiry._seconds))
    ? Number(rawExpiry._seconds) * 1000
    : Date.parse(rawExpiry);
  return !Number.isFinite(expiryMillis) || expiryMillis > Date.now();
}

function getEntitlementMaxPlayers() {
  const entitlement = currentEntitlements?.entitlement || currentEntitlements;
  if (!isCurrentEntitlementActive()) return 12;

  const value = Number(
    entitlement?.maxWindowsHostPlayers ??
    entitlement?.maxPlayersPerGame ??
    entitlement?.maxPlayers ??
    entitlement?.limits?.maxWindowsHostPlayers ??
    entitlement?.limits?.maxPlayersPerGame ??
    entitlement?.limits?.maxPlayers ??
    12
  );

  return Number.isFinite(value) && value > 0 ? value : 12;
}

function getEntitlementMaxServers() {
  const entitlement = currentEntitlements?.entitlement || currentEntitlements;
  if (!isCurrentEntitlementActive()) return 1;
  const value = Number(
    entitlement?.maxServers ??
    entitlement?.limits?.maxServers ??
    1
  );
  return Number.isFinite(value) && value >= 0 ? value : 1;
}

function normalizeGameModeId(value) {
  let clean = String(value || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
  for (const prefix of ["game_mode_", "mode_", "dlc_"]) {
    if (clean.startsWith(prefix)) clean = clean.slice(prefix.length);
  }
  if (clean.endsWith("_dlc")) clean = clean.slice(0, -4);
  return clean;
}

function isAllContentEntitlementToken(value) {
  const clean = normalizeGameModeId(value);
  return clean === "*" ||
    clean === "all" ||
    clean === "all_content" ||
    clean === "all_modes" ||
    clean === "all_game_modes" ||
    clean === "all_dlc" ||
    clean === "everything" ||
    clean === "full_access" ||
    clean === "founder";
}

function hasAllContentEntitlement() {
  const entitlement = currentEntitlements?.entitlement || currentEntitlements || {};
  const planActive = isCurrentEntitlementActive();
  const featureMap = entitlement?.features || entitlement?.customFields || {};
  const featureAll = planActive && (featureMap?.allContent === true ||
    featureMap?.allGameModes === true ||
    featureMap?.allDlc === true ||
    featureMap?.founderAccess === true);
  if (featureAll) return true;

  const allowed = planActive && Array.isArray(entitlement.allowedGameModes) ? entitlement.allowedGameModes : [];
  const includedModes = planActive && Array.isArray(entitlement.subscriptionIncludedGameModes) ? entitlement.subscriptionIncludedGameModes : [];
  const unlocked = Array.isArray(entitlement.unlockedGameModes) ? entitlement.unlockedGameModes : [];
  const themesAllowed = planActive && Array.isArray(entitlement.allowedThemes) ? entitlement.allowedThemes : [];
  const includedThemes = planActive && Array.isArray(entitlement.subscriptionIncludedThemes) ? entitlement.subscriptionIncludedThemes : [];
  const themesUnlocked = Array.isArray(entitlement.unlockedThemes) ? entitlement.unlockedThemes : [];
  return [...allowed, ...includedModes, ...unlocked, ...themesAllowed, ...includedThemes, ...themesUnlocked].some(isAllContentEntitlementToken);
}

function getEntitlementMinLocationUploadSeconds() {
  const entitlement = currentEntitlements?.entitlement || currentEntitlements;
  if (!isCurrentEntitlementActive()) return 20;

  const planId = String(entitlement?.planId || entitlement?.plan || "free").toLowerCase();

  const value = Number(
    entitlement?.minLocationUploadSeconds ??
    entitlement?.minimumLocationUploadSeconds ??
    entitlement?.limits?.minLocationUploadSeconds ??
    entitlement?.limits?.minimumLocationUploadSeconds ??
    entitlement?.customFields?.minLocationUploadSeconds
  );

  if (Number.isFinite(value) && value >= 5) {
    return Math.max(5, Math.min(60, value));
  }

  if (planId === "pro") return 10;
  if (planId === "plus") return 15;
  return 20;
}

function clampLocationUploadSecondsToEntitlement({ showError = false } = {}) {
  const input = document.getElementById("locationUploadSeconds");
  const help = document.getElementById("locationUploadSecondsHelp");
  if (!input) return;

  const minimum = getEntitlementMinLocationUploadSeconds();
  const planName = currentEntitlements?.planName || currentEntitlements?.planId || "Free";
  const requested = Number(input.value);

  input.min = String(minimum);

  if (help) {
    help.textContent = `${planName} minimum: ${minimum} seconds. Plus: 15s. Pro: 10s.`;
  }

  if (!Number.isFinite(requested) || requested < minimum) {
    input.value = String(minimum);
    if (showError) {
      showMessage(`Location upload seconds cannot be lower than your ${planName} entitlement minimum of ${minimum}.`, "error");
    }
  }
}

function normalizeThemeId(value) {
  return String(value || "")
    .trim()
    .toLowerCase()
    .replace(/_/g, "-");
}

function normalizeEntitlementKey(value) {
  return String(value || "")
    .trim()
    .toLowerCase()
    .replace(/[-\s]+/g, "_");
}

function isDlcTheme(themeId) {
  const normalized = normalizeThemeId(themeId);
  return DLC_THEME_IDS.includes(normalized);
}

function getEffectiveEntitlementThemes() {
  const entitlement = currentEntitlements?.entitlement || currentEntitlements || {};
  const planActive = isCurrentEntitlementActive();
  const allowed = planActive && Array.isArray(entitlement.allowedThemes) ? entitlement.allowedThemes : ["default"];
  const included = planActive && Array.isArray(entitlement.subscriptionIncludedThemes) ? entitlement.subscriptionIncludedThemes : [];
  const unlocked = Array.isArray(entitlement.unlockedThemes) ? entitlement.unlockedThemes : [];
  return [...new Set([...allowed, ...included, ...unlocked].map(value => String(value || "").trim()).filter(Boolean))];
}

function hasThemeEntitlement(themeId) {
  const normalizedTheme = normalizeThemeId(themeId);
  if (!normalizedTheme) return true;
  if (hasAllContentEntitlement()) return true;

  const normalizedKey = normalizeEntitlementKey(normalizedTheme);
  const acceptedKeys = new Set([
    normalizedTheme,
    normalizedKey,
    `theme_${normalizedKey}`,
    `${normalizedKey}_theme`
  ]);

  return getEffectiveEntitlementThemes().some(value =>
    acceptedKeys.has(normalizeThemeId(value)) ||
    acceptedKeys.has(normalizeEntitlementKey(value))
  );
}

function canHostTheme(themeId) {
  const normalizedTheme = normalizeThemeId(themeId);
  if (!normalizedTheme || normalizedTheme === "default") return true;

  // Existing built-in themes remain free. Only known standalone DLC themes are gated.
  if (!isDlcTheme(normalizedTheme)) return true;

  return hasThemeEntitlement(normalizedTheme);
}

function getSafeSelectedTheme(themeId, fallback = "default") {
  const normalizedFallback = canHostTheme(fallback) ? fallback : "default";
  return canHostTheme(themeId) ? (themeId || normalizedFallback) : normalizedFallback;
}

function renderThemeSelectAvailability() {
  const select = document.getElementById("themeSelect");
  if (!select) return;

  for (const option of Array.from(select.options)) {
    const themeId = option.value;
    if (!option.dataset.baseLabel) {
      option.dataset.baseLabel = option.textContent || themeId;
    }

    const locked = isDlcTheme(themeId) && !canHostTheme(themeId);
    option.disabled = locked;
    option.textContent = locked
      ? `${option.dataset.baseLabel.replace(/\s*\(Locked\)$/i, "")} (Locked)`
      : option.dataset.baseLabel;
  }

  if (!canHostTheme(select.value)) {
    const fallback = getSafeSelectedTheme(currentSyncSettings?.theme || currentServerConfig?.theme || "default");
    select.value = fallback;
    applyTheme(fallback);
  }
}

async function handleThemeSelectionChanged(select) {
  if (!select) return;

  const selectedTheme = select.value || "default";
  if (!canHostTheme(selectedTheme)) {
    const label = DLC_THEME_LABELS[normalizeThemeId(selectedTheme)] || selectedTheme;
    const fallback = getSafeSelectedTheme(currentSyncSettings?.theme || currentServerConfig?.theme || "default");
    select.value = fallback;
    applyTheme(fallback);
    showMessage(`${label} is a DLC theme. This host needs to own it before selecting it.`, "error");
    return;
  }

  applyTheme(selectedTheme);
  await saveTheme();
}

function renderEntitlements(entitlements) {
  currentEntitlements = entitlements?.entitlement || entitlements || currentEntitlements || {
    planId: "free",
    planName: "Free",
    maxPlayersPerGame: 8,
    maxAndroidHostPlayers: 8,
    maxWindowsHostPlayers: 12,
    minLocationUploadSeconds: 20
  };

  const maxPlayersInput = document.getElementById("maxPlayers");
  const maxPlayersHelp = document.getElementById("maxPlayersHelp");
  const maxAllowed = getEntitlementMaxPlayers();
  const planName = currentEntitlements?.planName || currentEntitlements?.planId || "free";

  if (maxPlayersInput) {
    maxPlayersInput.max = String(maxAllowed);
  }

  if (maxPlayersHelp) {
    const maxServers = getEntitlementMaxServers();
    const unlockedModes = getEffectiveEntitlementGameModes();
    const modeCount = unlockedModes.length;
    maxPlayersHelp.textContent = `Your ${planName} entitlement allows up to ${maxAllowed} players, ${maxServers} server${maxServers === 1 ? "" : "s"}, and ${modeCount} hostable mode${modeCount === 1 ? "" : "s"}.`;
  }

  if (maxPlayersInput) {
    const currentValue = Number(maxPlayersInput.value);

    if (!Number.isFinite(currentValue) || currentValue <= 0 || (currentValue === 5 && maxAllowed > 5)) {
      maxPlayersInput.value = String(maxAllowed);
    }
  }

  clampMaxPlayersToEntitlement();
  clampLocationUploadSecondsToEntitlement();
  renderThemeSelectAvailability();
  renderGameModeSelect();
  updateProximityTaggingAvailability();
}

function renderGameSettings(settings) {
  const maxAllowed = getEntitlementMaxPlayers();
  const savedMaxPlayers = Number(settings.maxPlayers);

  // Use entitlement max as the default. Older settings often return 5 even
  // when the entitlement allows more, so treat 5 as an old default if the
  // entitlement is higher.
  const shouldUseEntitlementDefault =
    !Number.isFinite(savedMaxPlayers) ||
    savedMaxPlayers <= 0 ||
    (savedMaxPlayers === 5 && maxAllowed > 5);

  setInputValue("gameDurationMinutes", settings.gameDurationMinutes ?? 45);
  setInputValue("maxPlayers", shouldUseEntitlementDefault ? maxAllowed : savedMaxPlayers);
  const savedProximityPreference = settings.savedAllowProximityTagging ?? settings.allowProximityTagging;
  setCheckboxValue("allowProximityTagging", savedProximityPreference !== false);
  setInputValue("proximityTagHoldSeconds", clampWholeNumber(settings.proximityTagHoldSeconds, 0, 5, 0));
  clampMaxPlayersToEntitlement();
  renderGameModeSettingsSection();
  updateProximityTaggingAvailability();
}



function getEffectiveEntitlementGameModes() {
  const entitlement = currentEntitlements?.entitlement || currentEntitlements || {};
  const planActive = isCurrentEntitlementActive();
  const allowed = planActive && Array.isArray(entitlement.allowedGameModes) ? entitlement.allowedGameModes : [];
  const included = planActive && Array.isArray(entitlement.subscriptionIncludedGameModes) ? entitlement.subscriptionIncludedGameModes : [];
  const unlocked = Array.isArray(entitlement.unlockedGameModes) ? entitlement.unlockedGameModes : [];
  const defaults = ["team_elimination", "capture_the_flag", "ghost_hunt"];
  const modes = [...new Set([...(allowed.length ? allowed : defaults), ...included, ...unlocked].map(normalizeGameModeId).filter(Boolean))];
  if (hasAllContentEntitlement()) {
    for (const mode of ["team_elimination", "capture_the_flag", "ghost_hunt", "zombie_extraction", "zombie_tag"]) {
      if (!modes.includes(mode)) modes.push(mode);
    }
  }
  return modes;
}

function canHostGameMode(modeId) {
  if (!modeId) return true;
  if (hasAllContentEntitlement()) return true;
  return getEffectiveEntitlementGameModes().includes(normalizeGameModeId(modeId));
}

function isBleForcedOffSelectedMode() {
  const mode = getSelectedGameModeDefinition()?.modeId || currentGame?.gameMode || "";
  const normalized = String(mode).toLowerCase();
  return normalized === "ghost_hunt" || normalized === ZOMBIE_EXTRACTION_MODE_ID;
}

function updateProximityTaggingAvailability() {
  const input = document.getElementById("allowProximityTagging");
  const holdInput = document.getElementById("proximityTagHoldSeconds");
  const help = document.querySelector(".proximity-tagging-help");
  if (!input) return;

  const forcedOff = isBleForcedOffSelectedMode();
  input.disabled = forcedOff;
  if (holdInput) {
    holdInput.disabled = forcedOff;
  }
  if (help) {
    help.textContent = forcedOff
      ? "BLE player tagging is disabled for this game mode. Your saved preference will return when another mode is selected."
      : "Allows nearby player tagging by Bluetooth. This preference is preserved when a mode temporarily disables BLE.";
  }
}

function getSelectedGameModeDefinition() {
  const selectedRulesFile =
    document.getElementById("gameModeSelect")?.value ||
    currentGame?.rulesFile ||
    "";

  return availableGameModes.find(mode => mode.rulesFile === selectedRulesFile) || null;
}

function isInsideGameModeSettings(element) {
  return !!element?.closest?.("#gameModeSettingsSection");
}

function renderGameModeSettingsSection() {
  const section = document.getElementById("gameModeSettingsSection");
  const fields = document.getElementById("gameModeSettingsFields");
  const help = document.getElementById("gameModeSettingsHelp");

  if (!section || !fields) {
    return;
  }

  const selectedMode = getSelectedGameModeDefinition();
  const definitions = selectedMode?.adminSettings || [];

  if (!selectedMode || definitions.length === 0) {
    section.classList.add("hidden");
    fields.innerHTML = "";
    return;
  }

  section.classList.remove("hidden");

  if (help) {
    help.textContent = `${selectedMode.displayName} has extra settings. These are saved with the game setup and used unless a future mode rule overrides them directly.`;
  }

  const values = getSavedGameModeSettings(selectedMode.modeId);
  const groups = new Map();
  for (const definition of definitions) {
    const group = definition.group || "Settings";
    if (!groups.has(group)) {
      groups.set(group, []);
    }
    groups.get(group).push(definition);
  }

  fields.innerHTML = [...groups.entries()].map(([group, groupDefinitions]) => `
    <div class="game-mode-settings-group">
      <h3>${escapeHtml(group)}</h3>
      <div class="admin-grid two-col">
        ${groupDefinitions.map(definition => renderGameModeSettingField(definition, values)).join("")}
      </div>
    </div>
  `).join("");
}

function renderGameModeSettingField(definition, values) {
  const key = definition.key || "";
  const inputType = definition.inputType || "number";
  const valueType = definition.valueType || inputType || "number";
  const savedValue = values[key] ?? definition.defaultValue ?? "";
  const description = definition.description
    ? `<span class="field-help muted">${escapeHtml(definition.description)}</span>`
    : "";

  if (inputType === "select") {
    const options = Array.isArray(definition.options) ? definition.options : [];
    const selectedValue = String(savedValue || definition.defaultValue || "");
    return `
      <label class="game-mode-setting-field">${escapeHtml(definition.label || key)}
        <select
          data-game-mode-setting-key="${escapeHtml(key)}"
          data-game-mode-setting-type="${escapeHtml(valueType)}"
        >
          ${options.map(option => {
            const optionValue = String(option.value ?? "");
            const optionLabel = String(option.label ?? optionValue);
            return `<option value="${escapeHtml(optionValue)}" ${optionValue === selectedValue ? "selected" : ""}>${escapeHtml(optionLabel)}</option>`;
          }).join("")}
        </select>
        ${description}
      </label>
    `;
  }

  if (inputType === "checkbox" || valueType === "boolean") {
    const checked = savedValue === true || String(savedValue).toLowerCase() === "true";
    return `
      <label class="checkbox-row game-mode-setting-field">
        <input
          type="checkbox"
          ${checked ? "checked" : ""}
          data-game-mode-setting-key="${escapeHtml(key)}"
          data-game-mode-setting-type="boolean"
        />
        <span>${escapeHtml(definition.label || key)}</span>
        ${description}
      </label>
    `;
  }

  const min = definition.min !== undefined && definition.min !== null ? ` min="${escapeHtml(definition.min)}"` : "";
  const max = definition.max !== undefined && definition.max !== null ? ` max="${escapeHtml(definition.max)}"` : "";
  const step = definition.step !== undefined && definition.step !== null ? ` step="${escapeHtml(definition.step)}"` : "";
  const htmlType = inputType === "text" ? "text" : "number";

  return `
    <label class="game-mode-setting-field">${escapeHtml(definition.label || key)}
      <input
        type="${htmlType}"
        value="${escapeHtml(savedValue)}"
        ${min}${max}${step}
        data-game-mode-setting-key="${escapeHtml(key)}"
        data-game-mode-setting-type="${escapeHtml(valueType)}"
      />
      ${description}
    </label>
  `;
}

function getSavedGameModeSettings(modeId) {
  const allSettings = currentSyncSettings?.gameModeSettings || {};
  if (!modeId) {
    return {};
  }

  return allSettings[modeId] || allSettings[modeId.toLowerCase()] || {};
}

function collectGameModeSettingsForSave() {
  const existing = { ...(currentSyncSettings?.gameModeSettings || {}) };
  const selectedMode = getSelectedGameModeDefinition();

  if (!selectedMode?.modeId) {
    return existing;
  }

  const values = { ...getSavedGameModeSettings(selectedMode.modeId) };

  document
    .querySelectorAll("#gameModeSettingsFields [data-game-mode-setting-key]")
    .forEach(input => {
      const key = input.dataset.gameModeSettingKey;
      const valueType = input.dataset.gameModeSettingType || "string";
      if (!key) {
        return;
      }

      if (valueType === "boolean") {
        values[key] = input.checked === true;
        return;
      }

      if (valueType === "number" || input.type === "number") {
        const number = Number(input.value);
        values[key] = Number.isFinite(number) ? number : 0;
        return;
      }

      values[key] = input.value;
    });

  existing[selectedMode.modeId] = values;
  return existing;
}

const GHOST_HUNT_PRESETS = {
  easy: {
    timeLimitMinutes: 18,
    playerHearts: 4,
    ghostsPerPlayerMin: 2,
    ghostsPerPlayerMax: 3,
    maxGhosts: 24,
    trapRangeMeters: 9.5,
    trapHoldSeconds: 2,
    dangerRangeMeters: 11,
    contactRangeMeters: 1.5,
    attackCooldownSeconds: 14,
    damageGraceSeconds: 10,
    averageGhostSpeedMetersPerSecond: 0.55,
    movingPlayerThresholdMetersPerTick: 0.65,
    stillHunterSafeRangeMeters: 14,
    ghostPatrolWaypointCount: 5,
    ghostPatrolWaypointReachMeters: 7,
    crowdScareRangeMeters: 11,
    crowdScarePlayerCount: 3,
    fastApproachMetersPerTick: 6,
    flickerPhaseChance: 0.045
  },
  normal: {
    timeLimitMinutes: 15,
    playerHearts: 3,
    ghostsPerPlayerMin: 3,
    ghostsPerPlayerMax: 5,
    maxGhosts: 30,
    trapRangeMeters: 8.25,
    trapHoldSeconds: 2,
    dangerRangeMeters: 14,
    contactRangeMeters: 2,
    attackCooldownSeconds: 10,
    damageGraceSeconds: 10,
    averageGhostSpeedMetersPerSecond: 0.75,
    movingPlayerThresholdMetersPerTick: 0.75,
    stillHunterSafeRangeMeters: 12,
    ghostPatrolWaypointCount: 6,
    ghostPatrolWaypointReachMeters: 6,
    crowdScareRangeMeters: 10,
    crowdScarePlayerCount: 3,
    fastApproachMetersPerTick: 6,
    flickerPhaseChance: 0.055
  },
  panic: {
    timeLimitMinutes: 12,
    playerHearts: 3,
    ghostsPerPlayerMin: 4,
    ghostsPerPlayerMax: 6,
    maxGhosts: 40,
    trapRangeMeters: 6.5,
    trapHoldSeconds: 3,
    dangerRangeMeters: 18,
    contactRangeMeters: 2.5,
    attackCooldownSeconds: 7,
    damageGraceSeconds: 10,
    averageGhostSpeedMetersPerSecond: 0.95,
    movingPlayerThresholdMetersPerTick: 0.45,
    stillHunterSafeRangeMeters: 11,
    ghostPatrolWaypointCount: 7,
    ghostPatrolWaypointReachMeters: 5,
    crowdScareRangeMeters: 12,
    crowdScarePlayerCount: 2,
    fastApproachMetersPerTick: 4,
    flickerPhaseChance: 0.080
  }
};

function applyGhostHuntPresetToFields(presetValue) {
  const preset = GHOST_HUNT_PRESETS[String(presetValue || "normal").toLowerCase()] || GHOST_HUNT_PRESETS.normal;
  for (const [key, value] of Object.entries(preset)) {
    const input = [...document.querySelectorAll("#gameModeSettingsFields [data-game-mode-setting-key]")]
      .find(candidate => candidate.dataset.gameModeSettingKey === key);
    if (!input) {
      continue;
    }

    if (input.type === "checkbox") {
      input.checked = value === true || String(value).toLowerCase() === "true";
    } else {
      input.value = String(value);
    }
  }

  showMessage("Ghost Hunt preset loaded. Press Save Game Settings to keep it.", "success");
}


const ZOMBIE_EXTRACTION_PRESETS = {
  casual: {
    timeLimitMinutes: 18,
    playerHealth: 4,
    damageShieldSeconds: 10,
    startingZombiesPerPlayerMin: 2,
    startingZombiesPerPlayerMax: 4,
    minimumStartingZombies: 4,
    maxActiveZombies: 80,
    reinforcementZombiesMin: 1,
    reinforcementZombiesMax: 1,
    reinforcementIntervalSeconds: 40,
    beaconPartsMin: 4,
    beaconPartsMax: 6,
    supplyCrateBonusMin: 2,
    supplyCrateBonusMax: 4,
    zombieSpeedMultiplier: 1.03,
    maximumZombieSpeedMetersPerSecond: 2.0,
    extractionHoldSeconds: 8
  },
  normal: {
    timeLimitMinutes: 15,
    playerHealth: 3,
    damageShieldSeconds: 8,
    startingZombiesPerPlayerMin: 3,
    startingZombiesPerPlayerMax: 5,
    minimumStartingZombies: 5,
    maxActiveZombies: 100,
    reinforcementZombiesMin: 1,
    reinforcementZombiesMax: 2,
    reinforcementIntervalSeconds: 30,
    beaconPartsMin: 4,
    beaconPartsMax: 8,
    supplyCrateBonusMin: 1,
    supplyCrateBonusMax: 4,
    zombieSpeedMultiplier: 1.08,
    maximumZombieSpeedMetersPerSecond: 2.25,
    extractionHoldSeconds: 10
  },
  panic: {
    timeLimitMinutes: 12,
    playerHealth: 3,
    damageShieldSeconds: 6,
    startingZombiesPerPlayerMin: 4,
    startingZombiesPerPlayerMax: 6,
    minimumStartingZombies: 6,
    maxActiveZombies: 100,
    reinforcementZombiesMin: 2,
    reinforcementZombiesMax: 3,
    reinforcementIntervalSeconds: 25,
    beaconPartsMin: 6,
    beaconPartsMax: 10,
    supplyCrateBonusMin: 0,
    supplyCrateBonusMax: 2,
    zombieSpeedMultiplier: 1.12,
    maximumZombieSpeedMetersPerSecond: 2.5,
    extractionHoldSeconds: 12
  }
};

function applyZombieExtractionPresetToFields(presetValue) {
  const preset = ZOMBIE_EXTRACTION_PRESETS[String(presetValue || "normal").toLowerCase()] || ZOMBIE_EXTRACTION_PRESETS.normal;
  for (const [key, value] of Object.entries(preset)) {
    const input = [...document.querySelectorAll("#gameModeSettingsFields [data-game-mode-setting-key]")]
      .find(candidate => candidate.dataset.gameModeSettingKey === key);
    if (!input) continue;
    input.value = String(value);
  }

  showMessage("Zombie Extraction preset loaded. Press Save Game Settings to keep it.", "success");
}

function clampMaxPlayersToEntitlement() {
  const input = document.getElementById("maxPlayers");
  if (!input) return;

  const maxAllowed = getEntitlementMaxPlayers();
  const requested = Number(input.value);

  if (!Number.isFinite(requested) || requested < 1) {
    input.value = "1";
    return;
  }

  if (requested > maxAllowed) {
    input.value = String(maxAllowed);
    showMessage(`Max players cannot be higher than your entitlement limit of ${maxAllowed}.`, "error");
  }
}

async function saveGameSettings() {
  try {
    clampMaxPlayersToEntitlement();

    const body = buildSafeSyncSettingsBody({
      gameDurationMinutes: Number(document.getElementById("gameDurationMinutes")?.value || 45),
      maxPlayers: Number(document.getElementById("maxPlayers")?.value || getEntitlementMaxPlayers()),
      gameModeSettings: collectGameModeSettingsForSave()
    });

    const result = await putJson(`/api/admin/game-settings`, body);

    currentSyncSettings = result.settings || body;
    clearAdminFormDirty();
    renderSyncSettings(currentSyncSettings);
    showMessage(result.message || "Game settings saved.", "success");
  } catch (error) {
    showMessage(error.message, "error");
  }
}

function minimumNumber(value, minimum, fallback = minimum) {
  const number = Number(value);
  return Number.isFinite(number) ? Math.max(minimum, number) : fallback;
}

function clampWholeNumber(value, minimum, maximum, fallback = minimum) {
  const number = Number(value);
  if (!Number.isFinite(number)) {
    return fallback;
  }

  return Math.min(maximum, Math.max(minimum, Math.round(number)));
}

function readMinimumNumberInput(id, minimum, fallback = minimum) {
  return minimumNumber(document.getElementById(id)?.value, minimum, fallback);
}

function readBoundedWholeNumberInput(id, minimum, maximum, fallback = minimum) {
  return clampWholeNumber(document.getElementById(id)?.value, minimum, maximum, fallback);
}

function clampProximityTagHoldSeconds() {
  const input = document.getElementById("proximityTagHoldSeconds");
  if (!input) {
    return;
  }

  input.value = String(readBoundedWholeNumberInput("proximityTagHoldSeconds", 0, 5, 0));
}

function markSyncSettingsDirty() {
  syncSettingsDirty = true;
  suppressSyncSettingsRenderUntil = Date.now() + 8000;
  adminFormDirtyUntil = Date.now() + ADMIN_EDIT_SUPPRESS_REFRESH_MS;
}

function clearSyncSettingsDirty() {
  syncSettingsDirty = false;
  suppressSyncSettingsRenderUntil = 0;
  clearAdminFormDirty();
}

function shouldSkipSyncSettingsRender() {
  return syncSettingsDirty || Date.now() < suppressSyncSettingsRenderUntil;
}

function buildSafeSyncSettingsBody(extra = {}) {
  return {
    syncIntervalSeconds: readMinimumNumberInput("syncIntervalSeconds", 5),
    locationUploadSeconds: readMinimumNumberInput("locationUploadSeconds", getEntitlementMinLocationUploadSeconds()),
    locationMinDistanceMeters: readMinimumNumberInput("locationMinDistanceMeters", 10),
    maxLocationAgeSeconds: readMinimumNumberInput("maxLocationAgeSeconds", 60),
    testingSyncIntervalSeconds: 5,
    useTestingIntervals: false,
    enableRealtimeCriticalUpdates: document.getElementById("enableRealtimeCriticalUpdates")?.checked !== false,
    allowProximityTagging: document.getElementById("allowProximityTagging")?.checked === true,
    proximityTagHoldSeconds: readBoundedWholeNumberInput("proximityTagHoldSeconds", 0, 5, 0),
    theme: document.getElementById("themeSelect")?.value || currentServerConfig?.theme || "default",
    gameDurationMinutes: Number(document.getElementById("gameDurationMinutes")?.value || 45),
    maxPlayers: Number(document.getElementById("maxPlayers")?.value || getEntitlementMaxPlayers()),
    gameModeSettings: currentSyncSettings?.gameModeSettings || {},
    ...extra
  };
}

function getLocalAdminThemePreference(...candidates) {
  // Phase 45C.3: explicit local/admin candidates must win over the last game
  // snapshot. During saveTheme(), currentGame can still contain the previous
  // theme until the next one-second admin poll, which caused the newly selected
  // theme to flash briefly and then snap back.
  const themeCandidates = [
    ...candidates,
    currentGame?.theme,
    currentGame?.Theme
  ];

  for (const candidate of themeCandidates) {
    const normalized = normalizeThemeId(candidate);
    if (normalized) {
      return getSafeSelectedTheme(normalized);
    }
  }

  return "default";
}

function renderServerConfig(config) {
  setInputValue("configServerName", config.serverName || "");
  const safeTheme = getLocalAdminThemePreference(config.theme, "default");
  setInputValue("themeSelect", safeTheme);
  renderThemeSelectAvailability();
  applyTheme(safeTheme);
}

function renderSyncSettings(settings) {
  if (settings) {
    currentSyncSettings = settings;
  }

  if (!settings || shouldSkipSyncSettingsRender()) {
    return;
  }

  setInputValue("syncIntervalSeconds", minimumNumber(settings.syncIntervalSeconds, 5));
  setInputValue("locationUploadSeconds", minimumNumber(settings.locationUploadSeconds, getEntitlementMinLocationUploadSeconds()));
  clampLocationUploadSecondsToEntitlement();
  setInputValue("locationMinDistanceMeters", minimumNumber(settings.locationMinDistanceMeters, 10));
  setInputValue("maxLocationAgeSeconds", minimumNumber(settings.maxLocationAgeSeconds, 60));

  setCheckboxValue("enableRealtimeCriticalUpdates", settings.enableRealtimeCriticalUpdates !== false);

  const safeTheme = getLocalAdminThemePreference(currentServerConfig?.theme, settings.theme, "default");
  setInputValue("themeSelect", safeTheme);
  renderThemeSelectAvailability();
  applyTheme(safeTheme);

  renderGameSettings(settings);
}


function toFriendlyName(value) {
  return String(value || "")
    .replace(/\.[^/.]+$/, "")
    .replace(/[_-]+/g, " ")
    .replace(/\s+/g, " ")
    .trim()
    .split(" ")
    .filter(Boolean)
    .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
    .join(" ");
}

function getFriendlyModeName(modeIdOrRulesFile) {
  const raw = String(modeIdOrRulesFile || "").trim();

  if (!raw) {
    return "Not Set";
  }

  const match = availableGameModes.find(mode =>
    String(mode.modeId || "").toLowerCase() === raw.toLowerCase() ||
    String(mode.id || "").toLowerCase() === raw.toLowerCase() ||
    String(mode.rulesFile || "").toLowerCase() === raw.toLowerCase()
  );

  return match?.displayName || toFriendlyName(raw);
}

function getFriendlyRulesName(rulesFile) {
  const raw = String(rulesFile || "").trim();

  if (!raw) {
    return "No Rules Set";
  }

  const match = availableGameModes.find(mode =>
    String(mode.rulesFile || "").toLowerCase() === raw.toLowerCase()
  );

  return match?.displayName || toFriendlyName(raw);
}

function getFriendlyThemeName(themeId) {
  const raw = String(themeId || "default").trim() || "default";
  const select = document.getElementById("themeSelect");
  const option = select
    ? Array.from(select.options || []).find(entry => String(entry.value || "").toLowerCase() === raw.toLowerCase())
    : null;
  return option?.textContent?.trim() || toFriendlyName(raw);
}


function renderPlatformStatusPanel() {
  const anchor = document.getElementById("adminMessage");
  if (!anchor) return;

  let panel = document.getElementById("platformStatusPanel");
  if (!panel) {
    panel = document.createElement("div");
    panel.id = "platformStatusPanel";
    panel.className = "platform-status-panel";
    anchor.parentNode.insertBefore(panel, anchor);
  }

  const banners = Array.isArray(currentPlatformStatus?.banners)
    ? currentPlatformStatus.banners
    : [];

  if (banners.length === 0) {
    panel.className = "platform-status-panel hidden";
    panel.innerHTML = "";
    return;
  }

  panel.className = "platform-status-panel";
  panel.innerHTML = banners.map(banner => {
    const severity = String(banner.severity || "info").toLowerCase();
    const title = banner.title || "Platform notice";
    const message = banner.message || "";
    return `
      <div class="platform-status-banner platform-status-${escapeHtml(severity)}">
        <strong>${escapeHtml(title)}</strong>
        ${message ? `<span>${escapeHtml(message)}</span>` : ""}
      </div>
    `;
  }).join("");
}

function renderAdminStatus() {
  const gameNameEl = document.getElementById("adminStatusGameName");
  const stateEl = document.getElementById("adminStatusState");
  const modeEl = document.getElementById("adminStatusMode");
  const themeEl = document.getElementById("adminStatusTheme");

  if (!gameNameEl || !stateEl || !modeEl || !themeEl || !currentGame) {
    return;
  }

  const status = currentGame.status || "Unknown";
  const mode = getFriendlyModeName(currentGame.gameMode || currentGame.rulesFile);
  const theme = getFriendlyThemeName(currentGame.theme || currentSyncSettings?.theme || currentServerConfig?.theme || document.getElementById("themeSelect")?.value || "default");

  gameNameEl.textContent = currentServerConfig?.serverName || currentGame.name || "Unnamed Server";
  stateEl.textContent = `Status: ${status}`;
  modeEl.textContent = `Mode: ${mode}`;
  themeEl.textContent = `Theme: ${theme}`;

  stateEl.className = "status-pill";
  modeEl.className = "status-pill";
  themeEl.className = "status-pill";

  const normalizedStatus = status.toLowerCase();

  if (normalizedStatus.includes("active") || normalizedStatus.includes("running")) {
    stateEl.classList.add("status-pill-good");
  } else if (normalizedStatus.includes("waiting") || normalizedStatus.includes("setup") || normalizedStatus.includes("ready")) {
    stateEl.classList.add("status-pill-waiting");
  } else if (normalizedStatus.includes("paused")) {
    stateEl.classList.add("status-pill-paused");
  } else if (normalizedStatus.includes("finished") || normalizedStatus.includes("ended")) {
    stateEl.classList.add("status-pill-ended");
  }
}

function renderJoinLobby() {
  renderJoinQr();
  renderJoinPlayers();
  updateJoinLobbyActions();
}

function updateJoinLobbyActions() {
  const status = (currentGame?.status || "").toLowerCase();
  const startButton = document.getElementById("actualStartGameButton");
  const cancelButton = document.getElementById("cancelReadyButton");

  if (startButton) {
    const platformBlocked = currentPlatformStatus?.hostingBlocked === true;
    startButton.disabled = status !== "ready" || platformBlocked;
    startButton.title = platformBlocked
      ? (currentPlatformStatus?.hostingBlockReason || "Hosting is blocked by Admin platform policy.")
      : (status === "ready" ? "" : "The server must be Ready before starting.");
  }

  if (cancelButton) {
    cancelButton.disabled = status !== "ready";
    cancelButton.classList.toggle("hidden", status !== "ready");
  }
}

function renderJoinPlayers() {
  const el = document.getElementById("joinPlayersList");
  if (!el) return;

  const players = currentGame.players || [];
  const mode = getModeKeyForCurrentGame();

  if (players.length === 0) {
    el.innerHTML = `<p class="muted">No players have joined yet.</p>`;
    return;
  }

  if (isTeamLobbyMode(mode)) {
    renderTeamModeJoinPlayers(el, players);
    return;
  }

  el.innerHTML = `
    <div class="join-players-two-column join-players-no-teams">
      ${players.map(player => renderJoinPlayerRow(player, { showTeam: false })).join("")}
    </div>
  `;
}

function renderTeamModeJoinPlayers(el, players) {
  const teams = getConfiguredTwoTeamList();
  const renderedTeamIds = new Set();
  const columns = teams.slice(0, 2).map((team, index) => {
    const teamId = team.teamId || "";
    renderedTeamIds.add(teamId.toLowerCase());
    const teamPlayers = players.filter(player => String(player.teamId || "").toLowerCase() === teamId.toLowerCase());
    return renderJoinTeamColumn(team, teamPlayers, index);
  });

  const unassigned = players.filter(player => {
    const teamId = String(player.teamId || "").toLowerCase();
    return !teamId || !renderedTeamIds.has(teamId);
  });

  while (columns.length < 2) {
    columns.push(renderJoinTeamColumn({ teamId: `team_${columns.length + 1}`, name: `Team ${columns.length + 1}` }, [], columns.length));
  }

  const unassignedHtml = unassigned.length > 0
    ? `<section class="join-team-column join-team-column-wide"><h4>Waiting for team</h4>${unassigned.map(player => renderJoinPlayerRow(player, { showTeam: false })).join("")}</section>`
    : "";

  el.innerHTML = `
    <div class="join-team-columns">
      ${columns.join("")}
      ${unassignedHtml}
    </div>
  `;
}

function renderJoinTeamColumn(team, players, index = 0) {
  const teamName = team.name || team.teamName || team.teamId || `Team ${index + 1}`;
  const color = normalizeHexColor(team.colorHex || team.color || team.teamColorHex) || getDefaultTeamColor(team, index);

  return `
    <section class="join-team-column" style="--team-color:${escapeHtml(color)};">
      <h4><span class="team-color-swatch"></span>${escapeHtml(teamName)}</h4>
      ${players.length === 0
        ? `<p class="muted join-team-empty">No players assigned yet.</p>`
        : players.map(player => renderJoinPlayerRow(player, { showTeam: false })).join("")}
    </section>
  `;
}

function renderJoinPlayerRow(player, { showTeam = false } = {}) {
  const hasSprite = player.avatarType === "sprite" && Number(player.avatarIndex) >= 0;
  const avatarHtml = hasSprite
    ? renderSpriteAvatarHtml(Number(player.avatarIndex), "join-player-avatar sprite-player-avatar")
    : `<div class="join-player-avatar fallback-avatar">${escapeHtml((player.handle || player.playerId || "?").slice(0, 1).toUpperCase())}</div>`;
  const statusText = `${player.isAlive === false ? "Dead" : "Ready"} — ${Number(player.stats?.score || player.score || 0)} pts`;
  const teamText = showTeam ? `Team: ${escapeHtml(getPlayerTeamDisplayName(player))} — ` : "";

  return `
    <div class="join-player-row">
      ${avatarHtml}
      <div>
        <strong>${escapeHtml(player.handle || player.playerId)}</strong>
        <div class="muted">${teamText}${escapeHtml(statusText)}</div>
      </div>
    </div>
  `;
}


function getPlayerTeamDisplayName(player) {
  if (player.teamName && String(player.teamName).trim()) {
    return player.teamName;
  }

  const teamId = player.teamId || "";

  if (!teamId) {
    return "None";
  }

  const matchingTeam = (currentGame?.teams || []).find(team =>
    (team.teamId || "").toLowerCase() === teamId.toLowerCase());

  return matchingTeam?.name || teamId;
}


function renderJoinQr() {
  const qrEl = document.getElementById("joinQrCode");
  const textEl = document.getElementById("joinQrText");

  if (!qrEl || !textEl) {
    return;
  }

  if (!currentServerConfig || !currentGame) {
    qrEl.innerHTML = `<p class="muted">Loading join QR...</p>`;
    textEl.textContent = "";
    return;
  }

  const serverId = currentServerConfig.serverId || "";
  const gameId = currentGame.gameId || GAME_ID;
  const joinCode = currentGame.joinCode || "ABCD1234";

  if (!serverId) {
    qrEl.innerHTML = `<p class="muted">Owner User ID is missing from server config.</p>`;
    textEl.textContent = "";
    return;
  }

  const joinUrl =
    `qrtaggame://join?serverId=${encodeURIComponent(serverId)}` +
    `&gameId=${encodeURIComponent(gameId)}` +
    `&joinCode=${encodeURIComponent(joinCode)}`;

  textEl.textContent = joinUrl;

  qrEl.innerHTML = `
    <img
      class="join-qr-image"
      src="https://api.qrserver.com/v1/create-qr-code/?size=260x260&data=${encodeURIComponent(joinUrl)}"
      alt="Join game QR code"
    />
  `;
}

function applyTheme(themeName) {
  const themeLink = document.getElementById("themeStylesheet");
  const normalizedThemeName = themeName || "default";

  document.body.dataset.adminTheme = normalizedThemeName;
  document.body.dataset.theme = normalizedThemeName;
  document.documentElement.dataset.adminTheme = normalizedThemeName;
  document.documentElement.dataset.theme = normalizedThemeName;

  document.body.className = document.body.className
    .split(/\s+/)
    .filter(name => name && !name.startsWith("admin-theme-") && !name.startsWith("theme-"))
    .concat([`admin-theme-${normalizedThemeName}`, `theme-${normalizedThemeName}`])
    .join(" ");

  document.documentElement.className = document.documentElement.className
    .split(/\s+/)
    .filter(name => name && !name.startsWith("admin-theme-") && !name.startsWith("theme-"))
    .concat([`admin-theme-${normalizedThemeName}`, `theme-${normalizedThemeName}`])
    .join(" ");

  if (!themeLink) return;

  if (!themeName || themeName === "default") {
    themeLink.removeAttribute("href");
    return;
  }

  themeLink.href = `/themes/${encodeURIComponent(themeName)}.css?v=44D49`;
}

async function saveTheme() {
  const select = document.getElementById("themeSelect");
  const selectedTheme = select?.value || "default";
  const previousTheme = getSafeSelectedTheme(currentSyncSettings?.theme || currentServerConfig?.theme || "default");

  if (!canHostTheme(selectedTheme)) {
    const label = DLC_THEME_LABELS[normalizeThemeId(selectedTheme)] || selectedTheme;
    if (select) select.value = previousTheme;
    applyTheme(previousTheme);
    showMessage(`${label} is a DLC theme. This host needs to own it before selecting it.`, "error");
    return;
  }

  try {
    suppressSyncSettingsRenderUntil = Date.now() + 8000;

    const configBody = {
      serverName: document.getElementById("configServerName")?.value?.trim() || currentServerConfig?.serverName || "",
      theme: selectedTheme
    };

    const configResult = await putJson(`/api/admin/server-config`, configBody);
    currentServerConfig = configResult.config || { ...currentServerConfig, ...configBody };

    const settingsBody = buildSafeSyncSettingsBody({
      theme: selectedTheme
    });

    const settingsResult = await putJson(`/api/admin/sync-settings`, settingsBody);

    clearSyncSettingsDirty();

    // Keep the in-browser game snapshot coherent with the server-side update.
    // Otherwise renderServerConfig/renderSyncSettings can immediately reapply
    // the previous currentGame theme before the next background refresh lands.
    if (currentGame) {
      currentGame.theme = selectedTheme;
      if (Object.prototype.hasOwnProperty.call(currentGame, "Theme")) {
        currentGame.Theme = selectedTheme;
      }
    }

    applyTheme(selectedTheme);
    renderServerConfig(currentServerConfig);
    renderSyncSettings(settingsResult.settings || settingsBody);

    showMessage("Theme saved locally and to Firestore.", "success");
  } catch (error) {
    if (select) select.value = previousTheme;
    applyTheme(previousTheme);
    showMessage(error.message, "error");
  }
}

function updateJoinLobbyTabState() {
  if (!currentGame) {
    return;
  }

  const status = (currentGame.status || "").toLowerCase();
  const lobbyEnabled =
    status === "ready" ||
    status === "active" ||
    status === "paused";

  const setupLocked =
    status === "ready" ||
    status === "active" ||
    status === "paused";

  document.querySelectorAll("[data-admin-tab]").forEach(button => {
    const tabName = button.dataset.adminTab;
    const isJoinLobby = tabName === "joinLobby";
    const lockedSetupTab = setupLocked && !isJoinLobby;

    button.disabled = isJoinLobby ? !lobbyEnabled : lockedSetupTab;
    button.classList.toggle("disabled-tab", button.disabled);
  });

  if (setupLocked && activeAdminTab !== "joinLobby") {
    activateAdminTab("joinLobby");
  }

  if (!lobbyEnabled && activeAdminTab === "joinLobby") {
    activateAdminTab("gameSettings");
  }
}

function getReadyRequirements() {
  const requirements = [];

  const mainZone = getMainPlayZone();
  const boundaryPoints = mainZone?.boundaryPoints || [];
  const hasPlayZone = !!mainZone && boundaryPoints.length >= 3;

  requirements.push({
    id: "playZone",
    label: "Play zone boundary created",
    met: hasPlayZone,
    message: "Draw and save a play zone with at least 3 boundary points."
  });

  const selectedRulesFile =
    document.getElementById("gameModeSelect")?.value ||
    currentGame?.rulesFile ||
    "";

  requirements.push({
    id: "gameMode",
    label: "Game mode selected",
    met: selectedRulesFile.trim().length > 0,
    message: "Choose a game mode."
  });

  const platformBlocked = currentPlatformStatus?.hostingBlocked === true;
  requirements.push({
    id: "platformStatus",
    label: platformBlocked ? "Admin platform policy allows hosting" : "Admin platform policy clear",
    met: !platformBlocked,
    message: currentPlatformStatus?.hostingBlockReason || "Hosting is blocked by Admin platform policy."
  });

  const selectedMode = availableGameModes.find(mode => mode.rulesFile === selectedRulesFile);
  const selectedModeId = (selectedMode?.modeId || currentGame?.gameMode || "").toLowerCase();

  if (selectedModeId === "team_elimination" || selectedModeId === CTF_MODE_ID) {
    const teams = currentGame?.teams || [];
    const uniqueTeamIds = new Set(
      teams
        .map(team => (team.teamId || "").trim().toLowerCase())
        .filter(teamId => teamId.length > 0)
    );

    requirements.push({
      id: "twoUniqueTeams",
      label: "Exactly 2 unique Team IDs created",
      met: uniqueTeamIds.size === 2,
      message: selectedModeId === CTF_MODE_ID
        ? "Capture the Flag requires exactly 2 unique Team IDs."
        : "Team Elimination requires exactly 2 unique Team IDs."
    });
  }

  if (selectedModeId === CTF_MODE_ID) {
    const ctfStatus = getCtfSetupReadinessStatus();

    for (const teamStatus of ctfStatus.teamStatuses) {
      requirements.push({
        id: `ctfBase_${teamStatus.teamId}`,
        label: `${teamStatus.displayName} home base placed inside play zone`,
        met: teamStatus.baseInside,
        message: `${teamStatus.displayName} needs a home base marker inside the active play zone.`
      });

      requirements.push({
        id: `ctfFlag_${teamStatus.teamId}`,
        label: `${teamStatus.displayName} flag placed inside play zone`,
        met: teamStatus.flagInside,
        message: `${teamStatus.displayName} needs a flag marker inside the active play zone.`
      });
    }

    requirements.push({
      id: "ctfRespectiveFlagBaseLinks",
      label: "Each CTF flag is matched to its own team base",
      met: ctfStatus.linksValid,
      message: "Each team must have one flag paired with that same team's home base."
    });
  }

  return requirements;
}


function getMainPlayZone() {
  const zones = currentGame?.zones || [];
  return zones.length > 0 ? zones[0] : null;
}

function getConfiguredTwoTeamList() {
  return (currentGame?.teams || [])
    .filter(team => (team?.teamId || "").trim().length > 0)
    .slice(0, 2);
}

function normalizeKey(value) {
  return (value || "").toString().trim().toLowerCase();
}

function normalizeHexColor(value) {
  const raw = (value || "").toString().trim();
  const match = raw.match(/^#?([0-9a-fA-F]{6})$/);
  return match ? `#${match[1].toLowerCase()}` : "";
}

function getDefaultTeamColor(team, index = 0) {
  const key = `${team?.teamId || ""} ${team?.name || ""}`.toLowerCase();
  if (key.includes("blue")) return "#45a3ff";
  if (key.includes("red")) return "#ff4c4c";
  return DEFAULT_TEAM_COLORS[index % DEFAULT_TEAM_COLORS.length];
}

function getTeamColorById(teamId) {
  const wanted = normalizeKey(teamId);
  const teams = currentGame?.teams || [];
  const index = teams.findIndex(team => normalizeKey(team?.teamId) === wanted);
  const team = index >= 0 ? teams[index] : null;

  if (!team) {
    return "";
  }

  return normalizeHexColor(team.colorHex || team.color || team.teamColorHex || team.customData?.colorHex || team.customFields?.colorHex) ||
    getDefaultTeamColor(team, index);
}

function getObjectiveTeamColor(objective) {
  const data = objective?.customData || {};
  const teamId = getCtfObjectiveTeamId(objective);
  const teamColor = getTeamColorById(teamId);
  if (teamColor) {
    return teamColor;
  }

  return normalizeHexColor(data["ctf.teamColor"] || data["ctf.flagTeamColor"] || data["ctf.baseTeamColor"]) || "#ffbd62";
}

function getTeamColorStyle(teamIdOrColor) {
  const color = normalizeHexColor(teamIdOrColor) || getTeamColorById(teamIdOrColor);
  return color ? ` style="--team-color:${escapeHtml(color)};"` : "";
}

function getCtfObjectiveTeamId(objective) {
  const data = objective?.customData || {};
  const type = (objective?.objectiveType || "").toLowerCase();

  if (type === CTF_FLAG_TYPE) {
    return data["ctf.flagTeamId"] || objective?.currentOwnerTeamId || "";
  }

  if (type === CTF_BASE_TYPE) {
    return data["ctf.baseTeamId"] || objective?.currentOwnerTeamId || "";
  }

  return objective?.currentOwnerTeamId || "";
}

function findCtfObjectiveForTeam(objectiveType, teamId) {
  const wantedTeamId = normalizeKey(teamId);
  return (currentGame?.objectives || []).find(objective => {
    if (!objective?.active) return false;
    if ((objective.objectiveType || "").toLowerCase() !== objectiveType) return false;
    return normalizeKey(getCtfObjectiveTeamId(objective)) === wantedTeamId;
  }) || null;
}

function getObjectiveHomeObjectiveId(objective) {
  const data = objective?.customData || {};
  return data["ctf.homeObjectiveId"] || "";
}

function getCtfSetupReadinessStatus() {
  const zone = getMainPlayZone();
  const teams = getConfiguredTwoTeamList();
  const teamStatuses = teams.map((team, index) => {
    const teamId = team.teamId || "";
    const displayName = team.name || teamId || `Team ${index + 1}`;
    const base = findCtfObjectiveForTeam(CTF_BASE_TYPE, teamId);
    const flag = findCtfObjectiveForTeam(CTF_FLAG_TYPE, teamId);
    const baseInside = !!base && isObjectiveInsidePlayZone(base, zone);
    const flagInside = !!flag && isObjectiveInsidePlayZone(flag, zone);
    const expectedHomeId = normalizeKey(base?.objectiveId || `ctf_base_${teamId}`);
    const actualHomeId = normalizeKey(getObjectiveHomeObjectiveId(flag));
    const flagLinkedToBase = !!base && !!flag && actualHomeId === expectedHomeId;

    return {
      teamId,
      displayName,
      base,
      flag,
      baseInside,
      flagInside,
      flagLinkedToBase
    };
  });

  return {
    teamStatuses,
    linksValid: teamStatuses.length === 2 && teamStatuses.every(status => status.flagLinkedToBase)
  };
}

function isObjectiveInsidePlayZone(objective, zone = getMainPlayZone()) {
  const lat = Number(objective?.latitude);
  const lon = Number(objective?.longitude);
  if (!Number.isFinite(lat) || !Number.isFinite(lon) || !zone) {
    return false;
  }

  return isPointInsidePlayZone(lat, lon, zone);
}

function isPointInsidePlayZone(latitude, longitude, zone = getMainPlayZone()) {
  if (!zone) return false;

  const boundaryPoints = zone.boundaryPoints || [];
  if (boundaryPoints.length >= 3) {
    return isPointInsidePolygon(latitude, longitude, boundaryPoints);
  }

  const centerLat = Number(zone.centerLatitude);
  const centerLon = Number(zone.centerLongitude);
  const radiusMeters = Number(zone.radiusMeters || 0);
  if (!Number.isFinite(centerLat) || !Number.isFinite(centerLon) || radiusMeters <= 0) {
    return false;
  }

  return distanceMeters(latitude, longitude, centerLat, centerLon) <= radiusMeters;
}

function isPointInsidePolygon(latitude, longitude, polygon) {
  if (!polygon || polygon.length < 3) return false;

  let inside = false;
  const x = Number(longitude);
  const y = Number(latitude);

  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    const xi = Number(polygon[i].longitude);
    const yi = Number(polygon[i].latitude);
    const xj = Number(polygon[j].longitude);
    const yj = Number(polygon[j].latitude);

    const intersects = ((yi > y) !== (yj > y)) &&
      (x < ((xj - xi) * (y - yi)) / ((yj - yi) || Number.EPSILON) + xi);

    if (intersects) {
      inside = !inside;
    }
  }

  return inside;
}

function distanceMeters(lat1, lon1, lat2, lon2) {
  const earthRadiusMeters = 6371000;
  const toRadians = value => value * Math.PI / 180;
  const dLat = toRadians(lat2 - lat1);
  const dLon = toRadians(lon2 - lon1);
  const startLat = toRadians(lat1);
  const endLat = toRadians(lat2);
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(startLat) * Math.cos(endLat) *
    Math.sin(dLon / 2) * Math.sin(dLon / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return earthRadiusMeters * c;
}

function getReadyBlockers() {
  return getReadyRequirements().filter(requirement => !requirement.met);
}

function renderReadyRequirements() {
  const panel = document.getElementById("readyRequirementsPanel");
  const readyButton = document.getElementById("startGameButton");

  if (!panel || !readyButton || !currentGame) {
    return;
  }

  const status = (currentGame.status || "").toLowerCase();
  const blockers = getReadyBlockers();
  const setupLocked = status === "ready" || status === "active" || status === "paused";

  readyButton.disabled = setupLocked || blockers.length > 0;
  readyButton.title = blockers.length > 0
    ? blockers.map(blocker => blocker.message).join("\n")
    : "";

  const requirements = getReadyRequirements();

  if (setupLocked) {
    panel.innerHTML = `
      <div class="ready-requirements ready-requirements-locked">
        <strong>Setup locked.</strong>
        <span>Players can join. Use Cancel from the Join Lobby to unlock settings.</span>
      </div>
    `;
    return;
  }

  panel.innerHTML = `
    <div class="ready-requirements ${blockers.length === 0 ? "ready-requirements-ok" : "ready-requirements-warning"}">
      <strong>${blockers.length === 0 ? "Ready requirements met." : "Ready requirements"}</strong>
      <ul>
        ${requirements.map(requirement => `
          <li class="${requirement.met ? "requirement-met" : "requirement-missing"}">
            <span class="requirement-icon">${requirement.met ? "✓" : "!"}</span>
            <span>${escapeHtml(requirement.label)}</span>
          </li>
        `).join("")}
      </ul>
    </div>
  `;
}

function shouldPreserveEditableValue(el) {
  return isAdminEditableElement(el) && (document.activeElement === el || el.dataset.adminDirty === "true");
}

function setInputValue(id, value, options = {}) {
  const el = document.getElementById(id);
  if (!el) {
    return;
  }

  if (!options.force && shouldPreserveEditableValue(el)) {
    return;
  }

  el.value = value;
}

function setCheckboxValue(id, value, options = {}) {
  const el = document.getElementById(id);
  if (!el) {
    return;
  }

  if (!options.force && shouldPreserveEditableValue(el)) {
    return;
  }

  el.checked = value;
}

async function saveServerConfig() {
  try {
    const currentConfig = await getJson(`/api/admin/server-config`);

    const body = {
      serverName: document.getElementById("configServerName").value.trim(),
      theme: document.getElementById("themeSelect")?.value || currentServerConfig?.theme || "default"
    };

    const result = await putJson(`/api/admin/server-config`, body);

    currentServerConfig = result.config || body;

    clearAdminFormDirty();
    showMessage(result.message || "Server config saved.", "success");
    renderServerConfig(currentServerConfig);
    renderAdminStatus();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function saveSyncSettings() {
  try {
    clampLocationUploadSecondsToEntitlement({ showError: true });
    suppressSyncSettingsRenderUntil = Date.now() + 8000;
    const body = buildSafeSyncSettingsBody();

    const result = await putJson(`/api/admin/sync-settings`, body);

    clearSyncSettingsDirty();
    showMessage(result.message || "Sync settings saved.", "success");
    renderSyncSettings(result.settings || body);
  } catch (error) {
    showMessage(error.message, "error");
  }
}

function renderGameModeSelect() {
  const select = document.getElementById("gameModeSelect");
  if (!select) return;
  const existingValue = select.value;

  select.innerHTML = availableGameModes.map(mode => {
    const owned = canHostGameMode(mode.modeId);
    const adminAvailable = mode.available !== false;
    const disabled = !owned || !adminAvailable;
    const reason = !adminAvailable
      ? (mode.unavailableReason || "Unavailable in Admin")
      : (!owned ? "DLC required" : "");
    const suffix = reason ? ` — ${reason}` : "";
    return `
      <option value="${escapeHtml(mode.rulesFile)}" ${disabled ? "disabled" : ""} title="${escapeHtml(reason)}">
        ${escapeHtml(mode.displayName)}${suffix} (${escapeHtml(mode.rulesFile)})
      </option>
    `;
  }).join("");

  const preferred = existingValue || currentGame?.rulesFile || "";
  if (preferred && [...select.options].some(option => option.value === preferred)) {
    select.value = preferred;
  }
  updateProximityTaggingAvailability();
}

function renderZones() {
  const el = document.getElementById("currentZonePanel");
  if (!el) {
    return;
  }
  const zones = currentGame.zones || [];
  currentZone = zones.length > 0 ? zones[0] : null;

  if (!currentZone) {
    el.innerHTML = `
      <div class="status error">
        No play zone created yet. Draw a boundary on the map before starting the game.
      </div>
    `;
    return;
  }

  const boundaryPoints = currentZone.boundaryPoints || [];

  el.innerHTML = `
    <div class="admin-item">
      <strong>${escapeHtml(currentZone.name)}</strong>
      <span>ID: ${escapeHtml(currentZone.zoneId)}</span>
      <span>Boundary: ${boundaryPoints.length} points</span>
      <span>Dynamic objectives: ${currentZone.allowDynamicObjectives ? "Yes" : "No"}</span>
    </div>
  `;
}

function renderSavedZones() {
  const el = document.getElementById("savedZonesList");

  if (!el) {
    return;
  }

  if (!savedZones || savedZones.length === 0) {
    el.innerHTML = `<p class="muted">No saved zones yet.</p>`;
    return;
  }

  el.innerHTML = savedZones.map(zone => {
    const isDefaultZone = zone.savedZoneId && zone.savedZoneId === lastUsedSavedZoneId;
    return `
    <div class="admin-item ${isDefaultZone ? "default-saved-zone" : ""}">
      <div class="admin-item-title">
        <strong>${escapeHtml(zone.name)}</strong>
        ${isDefaultZone ? `<span class="status-pill ready">Default</span>` : ""}
        <div class="button-row compact-buttons">
          <button class="mini-button" data-use-saved-zone="${escapeHtml(zone.savedZoneId)}">Use</button>
          <button class="mini-button danger-button" data-delete-saved-zone="${escapeHtml(zone.savedZoneId)}">Delete</button>
        </div>
      </div>
      <span>${zone.boundaryPoints?.length || 0} boundary points</span>
      <span>Dynamic objectives: ${zone.allowDynamicObjectives ? "Yes" : "No"}</span>
      ${isDefaultZone ? `<span>Used automatically for new lobbies.</span>` : ""}
    </div>
  `;
  }).join("");

  document.querySelectorAll("[data-use-saved-zone]").forEach(button => {
    button.addEventListener("click", () => {
      useSavedZone(button.dataset.useSavedZone);
    });
  });

  document.querySelectorAll("[data-delete-saved-zone]").forEach(button => {
    button.addEventListener("click", () => {
      deleteSavedZone(button.dataset.deleteSavedZone);
    });
  });
}

async function saveCurrentZonePreset() {
  try {
    let points = [];

    if (zoneDrawPoints.length >= 3) {
      points = zoneDrawPoints;
    } else if (currentZone?.boundaryPoints?.length >= 3) {
      points = currentZone.boundaryPoints;
    }

    if (points.length < 3) {
      throw new Error("Create or edit a play zone before saving it for future use.");
    }

    const body = {
      name: document.getElementById("zoneName").value.trim() || currentZone?.name || "Play Zone",
      boundaryPoints: points.map(point => ({
        latitude: Number(point.latitude),
        longitude: Number(point.longitude)
      })),
      allowDynamicObjectives: document.getElementById("zoneAllowDynamic").checked
    };

    const result = await postJson(`/api/saved-zones`, body);

    showMessage(result.message || "Zone saved for future use.", "success");

    const savedZonesResponse = await getJson(`/api/saved-zones`);
    savedZones = savedZonesResponse.savedZones || [];
    lastUsedSavedZoneId = savedZonesResponse.lastUsedZone?.savedZoneId || lastUsedSavedZoneId || "";
    renderSavedZones();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function useSavedZone(savedZoneId) {
  const zone = savedZones.find(z => z.savedZoneId === savedZoneId);

  if (!zone) {
    showMessage("Saved zone not found.", "error");
    return;
  }

  try {
    clearZoneDraft();

    document.getElementById("zoneName").value = zone.name || "Play Zone";
    document.getElementById("zoneAllowDynamic").checked = zone.allowDynamicObjectives !== false;

    zoneDrawPoints = (zone.boundaryPoints || []).map(point => ({
      latitude: Number(point.latitude),
      longitude: Number(point.longitude)
    }));

    if (zoneDrawPoints.length < 3) {
      throw new Error("Saved zone must have at least 3 boundary points.");
    }

    const body = {
      zoneId: zone.savedZoneId,
      name: zone.name || "Play Zone",
      boundaryPoints: zoneDrawPoints.map(point => ({
        latitude: Number(point.latitude),
        longitude: Number(point.longitude)
      })),
      centerLatitude: null,
      centerLongitude: null,
      radiusMeters: 0,
      allowDynamicObjectives: zone.allowDynamicObjectives !== false
    };

    const result = await postJson(`/api/games/${GAME_ID}/zones`, body);

    currentZone = result.zone;
    lastUsedSavedZoneId = result.lastUsedZone?.savedZoneId || zone.savedZoneId || lastUsedSavedZoneId;
    mapClickMode = "none";

    showMessage(result.message || "Saved zone activated for this game and set as the default for new lobbies.", "success");
    updateMapHelp("Saved zone activated as the current play zone.");
    clearZoneDraft();
    await loadAdminData({ preserveMapView: true, quiet: true });
  } catch (error) {
    showMessage(error.message, "error");
  }
}


async function deleteSavedZone(savedZoneId) {
  const confirmed = await showAdminConfirm({
    title: "Delete saved zone?",
    message: "This removes the saved preset only. The current game zone will stay in place.",
    confirmText: "Delete Zone",
    cancelText: "Keep Zone",
    danger: true
  });

  if (!confirmed) {
    return;
  }

  try {
    const result = await deleteJson(`/api/saved-zones/${savedZoneId}`);
    showMessage(result.message || "Saved zone deleted.", "success");

    const savedZonesResponse = await getJson(`/api/saved-zones`);
    savedZones = savedZonesResponse.savedZones || [];
    lastUsedSavedZoneId = savedZonesResponse.lastUsedZone?.savedZoneId || "";
    renderSavedZones();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

function renderObjectives() {
  const el = document.getElementById("objectivesList");
  if (!el) {
    return;
  }

  renderCtfSetupPanel();

  const objectives = currentGame.objectives || [];

  if (objectives.length === 0) {
    el.innerHTML = isCaptureTheFlagMode()
      ? `<p class="muted">No CTF setup objectives yet. Activate a play zone, or re-select Capture the Flag, to create the team bases and GPS flags.</p>`
      : `<p class="muted">No objectives in this game session.</p>`;
    return;
  }

  el.innerHTML = objectives.map(objective => renderObjectiveItem(objective)).join("");

  document.querySelectorAll("[data-deactivate-objective]").forEach(button => {
    button.addEventListener("click", async () => {
      const objectiveId = button.dataset.deactivateObjective;
      await deactivateObjective(objectiveId);
    });
  });

  document.querySelectorAll("[data-objective-location-save]").forEach(button => {
    button.addEventListener("click", async () => {
      const objectiveId = button.dataset.objectiveLocationSave;
      await saveObjectiveLocationFromPanel(objectiveId);
    });
  });

  document.querySelectorAll("[data-objective-center]").forEach(button => {
    button.addEventListener("click", () => {
      centerMapOnObjective(button.dataset.objectiveCenter);
    });
  });
}

function renderCtfSetupPanel() {
  const panel = document.getElementById("ctfSetupPanel");
  if (!panel) return;

  if (!isCaptureTheFlagMode()) {
    panel.classList.add("hidden");
    panel.innerHTML = "";
    return;
  }

  const objectives = currentGame?.objectives || [];
  const ctfObjectives = objectives.filter(isCtfObjective);
  const hasZone = (currentGame?.zones || [])[0]?.boundaryPoints?.length >= 3;

  panel.classList.remove("hidden");
  panel.innerHTML = `
    <div class="ctf-setup-card">
      <strong>Capture the Flag setup</strong>
      <p>
        After the play zone is activated, the server creates two GPS bases and two GPS flags here before the game starts.
        Drag the flag/base markers on the map or fine-tune their latitude/longitude below.
      </p>
      <span>${hasZone ? "Play zone ready." : "Activate a play zone first."}</span>
      <span>${ctfObjectives.length >= 4 ? "CTF bases and flags are ready to tune." : "CTF bases and flags will appear after the play zone is activated."}</span>
    </div>
  `;
}

function renderObjectiveItem(objective) {
  const isCtf = isCtfObjective(objective);
  const editableLocation = canEditObjectiveSetupLocation(objective);
  const roleLabel = getObjectiveRoleLabel(objective);
  const teamLabel = getObjectiveTeamLabel(objective);
  const activeText = objective.active ? "Yes" : "No";
  const deactivateButton = isCtf
    ? ""
    : `<button class="mini-button danger-button" data-deactivate-objective="${escapeHtml(objective.objectiveId)}">Deactivate</button>`;

  const setupControls = editableLocation
    ? `
      <div class="admin-grid two-col objective-location-grid" data-objective-location-fields="true">
        <label>Latitude
          <input type="number" step="0.000001" value="${escapeHtml(objective.latitude)}" data-objective-lat />
        </label>
        <label>Longitude
          <input type="number" step="0.000001" value="${escapeHtml(objective.longitude)}" data-objective-lon />
        </label>
        <label>Radius Meters
          <input type="number" min="1" step="1" value="${escapeHtml(objective.radiusMeters)}" data-objective-radius />
        </label>
        <div class="button-row compact-buttons objective-location-actions">
          <button class="mini-button" type="button" data-objective-location-save="${escapeHtml(objective.objectiveId)}">Save Location</button>
          <button class="mini-button secondary" type="button" data-objective-center="${escapeHtml(objective.objectiveId)}">Center Map</button>
        </div>
      </div>
      <span class="muted">Tip: you can also drag this marker directly on the map before the game starts.</span>
    `
    : isCtf
      ? `<span class="muted">CTF setup markers can only be moved before the game is active.</span>`
      : "";

  return `
    <div class="admin-item ${isCtf ? "ctf-objective-item" : ""}" data-objective-id="${escapeHtml(objective.objectiveId)}">
      <div class="admin-item-title">
        <strong>${escapeHtml(objective.name)}</strong>
        <div class="button-row compact-buttons">
          ${deactivateButton}
          ${!editableLocation ? `<button class="mini-button secondary" type="button" data-objective-center="${escapeHtml(objective.objectiveId)}">Center Map</button>` : ""}
        </div>
      </div>
      ${isCtf ? `<span class="ctf-objective-badge">${escapeHtml(roleLabel)}${teamLabel ? ` · ${escapeHtml(teamLabel)}` : ""}</span>` : ""}
      <span>ID: ${escapeHtml(objective.objectiveId)}</span>
      <span>Type: ${escapeHtml(objective.objectiveType)}</span>
      <span>Source: ${escapeHtml(objective.source)}</span>
      <span>Active: ${activeText}</span>
      <span>Location: ${objective.latitude}, ${objective.longitude}</span>
      <span>Radius: ${objective.radiusMeters}m — Points: ${objective.pointValue}</span>
      <span>Claim Limit: ${escapeHtml(objective.claimLimit)} — Claims: ${(objective.claims || []).length}</span>
      <span>Owner Team: ${escapeHtml(objective.currentOwnerTeamId || "Neutral")}</span>
      ${setupControls}
    </div>
  `;
}


function isCaptureTheFlagMode() {
  const mode = (currentGame?.gameMode || "").toLowerCase();
  const selectedRulesFile = (document.getElementById("gameModeSelect")?.value || currentGame?.rulesFile || "").toLowerCase();
  return mode === CTF_MODE_ID || selectedRulesFile.includes("capture-the-flag");
}

function isLiveGameStatus() {
  return (currentGame?.status || "").toString().toLowerCase() === "active";
}

function isCtfObjective(objective) {
  const type = (objective?.objectiveType || "").toLowerCase();
  const id = (objective?.objectiveId || "").toLowerCase();
  return type === CTF_FLAG_TYPE || type === CTF_BASE_TYPE || id.startsWith("ctf_flag_") || id.startsWith("ctf_base_");
}

function canEditObjectiveSetupLocation(objective) {
  return isCtfObjective(objective) && !isLiveGameStatus();
}

function getObjectiveRoleLabel(objective) {
  const type = (objective?.objectiveType || "").toLowerCase();
  if (type === CTF_FLAG_TYPE) return "GPS Flag";
  if (type === CTF_BASE_TYPE) return "Home Base";
  return "Objective";
}

function getObjectiveTeamLabel(objective) {
  const data = objective?.customData || {};
  return data["ctf.flagTeamName"] || data["ctf.baseTeamName"] || getCtfObjectiveTeamId(objective) || "";
}

function getCtfObjectiveTeamColorClass(objective) {
  if (!isCtfObjective(objective)) {
    return "";
  }

  const teamId = normalizeKey(getCtfObjectiveTeamId(objective));
  const teamName = normalizeKey(getObjectiveTeamLabel(objective));

  if (teamId.includes("red") || teamName.includes("red")) {
    return "map-ctf-team-red";
  }

  if (teamId.includes("blue") || teamName.includes("blue")) {
    return "map-ctf-team-blue";
  }

  const teams = getConfiguredTwoTeamList();
  if (teams.length >= 1 && normalizeKey(teams[0].teamId) === teamId) {
    return "map-ctf-team-red";
  }

  if (teams.length >= 2 && normalizeKey(teams[1].teamId) === teamId) {
    return "map-ctf-team-blue";
  }

  return "";
}

function getObjectiveById(objectiveId) {
  return (currentGame?.objectives || []).find(o =>
    (o.objectiveId || "").toLowerCase() === (objectiveId || "").toLowerCase());
}

async function saveObjectiveLocationFromPanel(objectiveId) {
  const item = document.querySelector(`[data-objective-id="${cssEscape(objectiveId)}"]`);
  if (!item) {
    showMessage("Objective panel not found.", "error");
    return;
  }

  const latitude = Number(item.querySelector("[data-objective-lat]")?.value);
  const longitude = Number(item.querySelector("[data-objective-lon]")?.value);
  const radiusMeters = Number(item.querySelector("[data-objective-radius]")?.value);

  await saveObjectiveLocation(objectiveId, latitude, longitude, radiusMeters, { preserveMapView: true });
}

async function saveObjectiveLocation(objectiveId, latitude, longitude, radiusMeters, options = {}) {
  if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
    showMessage("Objective latitude and longitude are required.", "error");
    return;
  }

  if (radiusMeters !== undefined && radiusMeters !== null && !Number.isFinite(Number(radiusMeters))) {
    showMessage("Objective radius must be a number.", "error");
    return;
  }

  objectiveMoveInProgress = true;
  adminFormDirtyUntil = Date.now() + 3000;

  try {
    const body = {
      latitude,
      longitude,
      radiusMeters: Number.isFinite(Number(radiusMeters)) ? Number(radiusMeters) : null
    };

    const result = await putJson(`/api/games/${GAME_ID}/objectives/${encodeURIComponent(objectiveId)}/location`, body);
    showMessage(result.message || "Objective location updated.", "success");
    await loadAdminData({ preserveMapView: options.preserveMapView !== false, quiet: true });
  } catch (error) {
    showMessage(error.message, "error");
    await loadAdminData({ preserveMapView: true, quiet: true });
  } finally {
    objectiveMoveInProgress = false;
  }
}

function centerMapOnObjective(objectiveId) {
  const objective = getObjectiveById(objectiveId);
  if (!objective || !adminMap) {
    showMessage("Objective not found on the map.", "error");
    return;
  }

  const lat = Number(objective.latitude);
  const lon = Number(objective.longitude);
  if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
    showMessage("Objective location is not valid.", "error");
    return;
  }

  adminMap.setView([lat, lon], Math.max(adminMap.getZoom(), 18));
  updateMapHelp(`Centered on ${objective.name}.`);
}

function cssEscape(value) {
  if (window.CSS?.escape) {
    return CSS.escape(value || "");
  }

  return (value || "").replace(/[^A-Za-z0-9_-]/g, "\\$&");
}


function renderPlayers() {
  const el = document.getElementById("playersList");
  if (!el) {
    return;
  }

  const players = currentGame.players || [];
  const badges = currentGame.playerBadges || [];

  if (players.length === 0) {
    el.innerHTML = `<p class="muted">No players have joined yet.</p>`;
    return;
  }

  const mode = canonicalGameModeKey(currentGame.gameMode || currentGame.mode || currentGame.gameModeId);
  const zombieTagPregame = mode === ZOMBIE_TAG_MODE_ID && !["active", "paused", "finished"].includes(String(currentGame.status || "").toLowerCase());

  el.innerHTML = players.map(player => {
    const badge = badges.find(b => b.playerId === player.playerId);
    const roleLine = zombieTagPregame
      ? `<span>Role: picked randomly when the round starts</span>`
      : `<span>Team: ${escapeHtml(player.teamId)}</span>`;
    return `
      <div class="admin-item">
        <strong>${escapeHtml(player.handle)}</strong>
        <span>Player ID: ${escapeHtml(player.playerId)}</span>
        ${roleLine}
        <span>Badge: ${escapeHtml(badge?.badgeCode || "No badge")}</span>
        ${badge ? `<span>Badge URL: /scan/${escapeHtml(badge.badgeCode)}</span>` : ""}
      </div>
    `;
  }).join("");
}

function renderTeams() {
  const el = document.getElementById("teamsList");
  if (!el) return;

  const teams = currentGame.teams || [];
  const players = currentGame.players || [];
  const mode = canonicalGameModeKey(currentGame.gameMode || currentGame.mode || currentGame.gameModeId);
  const zombieTagPregame = mode === ZOMBIE_TAG_MODE_ID && !["active", "paused", "finished"].includes(String(currentGame.status || "").toLowerCase());

  if (zombieTagPregame) {
    el.innerHTML = `<p class="muted">Zombie Tag does not use lobby teams. Survivors and zombies are assigned when the round starts.</p>`;
    return;
  }

  if (teams.length === 0) {
    el.innerHTML = `<p class="muted">No teams configured yet.</p>`;
    return;
  }

  el.innerHTML = teams.map(team => renderTeamAdminItem(team, players)).join("");
}

function renderTeamAdminItem(team, players) {
  const teamPlayers = players.filter(player => player.teamId === team.teamId);
  const playerNames = teamPlayers.length > 0
    ? teamPlayers.map(player => escapeHtml(player.handle || player.playerId)).join(", ")
    : "No players assigned";

  if (editingTeamId && team.teamId === editingTeamId) {
    const currentNameInput = document.getElementById("teamEditName");
    const currentIdInput = document.getElementById("teamEditId");
    const currentColorInput = document.getElementById("teamEditColor");
    const draftName = currentNameInput ? currentNameInput.value : (team.name || team.teamId);
    const draftTeamId = currentIdInput ? currentIdInput.value : team.teamId;
    const draftColor = normalizeHexColor(currentColorInput ? currentColorInput.value : (team.colorHex || getTeamColorById(team.teamId))) || getTeamColorById(team.teamId) || "#ff4c4c";

    return `
      <div class="admin-item team-admin-item team-admin-item-editing">
        <div class="team-edit-fields">
          <label>
            Team Name
            <input id="teamEditName" type="text" value="${escapeHtml(draftName)}" />
          </label>
          <label>
            Team ID
            <input id="teamEditId" type="text" value="${escapeHtml(draftTeamId)}" />
          </label>
          <label>
            Team Color
            <input id="teamEditColor" class="team-color-picker" type="color" value="${escapeHtml(draftColor)}" />
          </label>
        </div>

        <div class="team-edit-actions">
          <button
            class="primary team-save-button"
            type="button"
            data-save-team-id="${escapeHtml(team.teamId)}">
            Save
          </button>
          <button
            class="secondary team-cancel-button"
            type="button"
            data-cancel-team-edit="true">
            Cancel
          </button>
        </div>

        <span>Score: ${team.score ?? 0}</span>
        <span>Players: ${playerNames}</span>
      </div>
    `;
  }

  return `
    <div class="admin-item team-admin-item">
      <div class="admin-item-title">
        <strong><span class="team-color-swatch"${getTeamColorStyle(team.colorHex || team.teamId)}></span>${escapeHtml(team.name || team.teamId)}</strong>
        <button
          class="secondary team-edit-button"
          type="button"
          data-edit-team-id="${escapeHtml(team.teamId)}">
          Edit
        </button>
      </div>
      <span>Team ID: ${escapeHtml(team.teamId)}</span>
      <span>Color: ${escapeHtml(normalizeHexColor(team.colorHex) || getTeamColorById(team.teamId) || "Default")}</span>
      <span>Score: ${team.score ?? 0}</span>
      <span>Players: ${playerNames}</span>
    </div>
  `;
}


function startEditTeam(teamId) {
  editingTeamId = teamId || "";
  renderTeams();

  window.setTimeout(() => {
    document.getElementById("teamEditName")?.focus();
    document.getElementById("teamEditName")?.select();
  }, 0);
}

function cancelTeamEdit() {
  editingTeamId = "";
  renderTeams();
}

async function saveTeamEdit(originalTeamId) {
  const team = (currentGame?.teams || []).find(item =>
    (item.teamId || "").toLowerCase() === (originalTeamId || "").toLowerCase());

  if (!team) {
    showMessage("Team not found.", "error");
    return;
  }

  const nameInput = document.getElementById("teamEditName");
  const teamIdInput = document.getElementById("teamEditId");
  const colorInput = document.getElementById("teamEditColor");

  const name = nameInput?.value?.trim() || "";
  const teamId = teamIdInput?.value?.trim() || "";
  const colorHex = normalizeHexColor(colorInput?.value || team.colorHex || getTeamColorById(team.teamId)) || getTeamColorById(team.teamId) || "#ff4c4c";

  if (!name) {
    showMessage("Team name cannot be blank.", "error");
    nameInput?.focus();
    return;
  }

  if (!teamId) {
    showMessage("Team ID cannot be blank.", "error");
    teamIdInput?.focus();
    return;
  }

  if (!/^[A-Za-z0-9_-]+$/.test(teamId)) {
    showMessage("Team ID can only use letters, numbers, hyphens, and underscores.", "error");
    teamIdInput?.focus();
    return;
  }

  try {
    const result = await putJson(
      `/api/games/${GAME_ID}/teams/${encodeURIComponent(originalTeamId)}`,
      {
        name,
        teamId,
        colorHex
      }
    );

    editingTeamId = "";
    showMessage(result.message || "Team updated.", "success");
    await loadAdminData();
  } catch (error) {
    showMessage(error.message, "error");
  }
}


function renderMapData(options = {}) {
  if (!adminMap) return;

  zoneLayer.clearLayers();
  objectiveLayer.clearLayers();

  const bounds = [];

  for (const zone of currentGame.zones || []) {
    const boundaryPoints = zone.boundaryPoints || [];

    if (boundaryPoints.length >= 3) {
      const latLngs = boundaryPoints.map(p => [Number(p.latitude), Number(p.longitude)]);
      const polygon = L.polygon(latLngs, { color: "#ffbd62", fillColor: "#ffbd62", weight: 3, fillOpacity: 0.18 }).addTo(zoneLayer);
      polygon.bindPopup(`<strong>${escapeHtml(zone.name)}</strong><br>${boundaryPoints.length} point boundary`);
      latLngs.forEach(point => bounds.push(point));
    } else if (Number.isFinite(Number(zone.centerLatitude)) && Number.isFinite(Number(zone.centerLongitude))) {
      const circle = L.circle([Number(zone.centerLatitude), Number(zone.centerLongitude)], {
        radius: Number(zone.radiusMeters || 250),
        weight: 2,
        fillOpacity: 0.08
      }).addTo(zoneLayer);
      circle.bindPopup(`<strong>${escapeHtml(zone.name)}</strong><br>Circle fallback zone`);
      bounds.push([Number(zone.centerLatitude), Number(zone.centerLongitude)]);
    }
  }

  for (const objective of currentGame.objectives || []) {
    if (!objective.active) continue;

    const lat = Number(objective.latitude);
    const lon = Number(objective.longitude);
    if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;

    const editableLocation = canEditObjectiveSetupLocation(objective);
    const marker = L.marker([lat, lon], {
      icon: createObjectiveIcon(objective),
      draggable: editableLocation
    }).addTo(objectiveLayer);

    marker.bindPopup(`
      <strong>${escapeHtml(objective.name)}</strong><br>
      ${escapeHtml(getObjectiveRoleLabel(objective))}<br>
      ${escapeHtml(getObjectiveTeamLabel(objective) || objective.objectiveType)}<br>
      ${editableLocation ? "Drag to fine-tune setup location" : `${objective.pointValue} points`}
    `);

    if (editableLocation) {
      marker.on("dragstart", () => {
        objectiveMoveInProgress = true;
        updateMapHelp(`Moving ${objective.name}. Drop the marker inside the play zone.`);
      });

      marker.on("dragend", async event => {
        const latLng = event.target.getLatLng();
        await saveObjectiveLocation(
          objective.objectiveId,
          latLng.lat,
          latLng.lng,
          Number(objective.radiusMeters || 25),
          { preserveMapView: true }
        );
      });
    }

    bounds.push([lat, lon]);
  }

  const shouldPreserveView =
    options.preserveMapView === true ||
    (activeAdminTab === "playZone" && adminMap._playZoneHasAutoFit === true);

  if (!shouldPreserveView && bounds.length > 0) {
    adminMap.fitBounds(bounds, { padding: [30, 30], maxZoom: 16 });
    adminMap._playZoneHasAutoFit = true;
  }

  window.setTimeout(() => adminMap.invalidateSize(), 50);
}

function startZoneDrawing() {
  clearZoneDraft();
  mapClickMode = "zone";
  updateMapHelp("Drawing play zone: click the map to add points. Drag points to adjust. Add at least 3 points, then click Activate Play Zone.");
}

function clearZoneDraft() {
  zoneDrawPoints = [];
  zoneVertexMarkers = [];
  draftLayer.clearLayers();
  zoneDraftPolygon = null;
  mapClickMode = "none";
  updateMapHelp("Boundary draft cleared.");
}

function addZoneDraftPoint(latlng) {
  const index = zoneDrawPoints.length;
  zoneDrawPoints.push({ latitude: latlng.lat, longitude: latlng.lng });

  const marker = L.marker(latlng, { draggable: true, icon: createVertexIcon(index + 1) }).addTo(draftLayer);
  marker.on("drag", event => {
    const markerLatLng = event.target.getLatLng();
    zoneDrawPoints[index] = {
      latitude: markerLatLng.lat,
      longitude: markerLatLng.lng
    };
    updateZoneDraftPolygon();
  });

  zoneVertexMarkers.push(marker);
  updateZoneDraftPolygon();
  updateZoneFallbackCenterFromDraft();
}

function updateZoneDraftPolygon() {
  if (zoneDraftPolygon) {
    draftLayer.removeLayer(zoneDraftPolygon);
  }

  if (zoneDrawPoints.length < 2) {
    zoneDraftPolygon = null;
    return;
  }

  const latLngs = zoneDrawPoints.map(p => [Number(p.latitude), Number(p.longitude)]);
  zoneDraftPolygon = L.polygon(latLngs, { color: "#ffbd62", fillColor: "#ffbd62", weight: 3, dashArray: "6,6", fillOpacity: 0.24 }).addTo(draftLayer);
}

function updateZoneFallbackCenterFromDraft() {
  if (zoneDrawPoints.length === 0) return;

  const avgLat = zoneDrawPoints.reduce((sum, p) => sum + Number(p.latitude), 0) / zoneDrawPoints.length;
  const avgLon = zoneDrawPoints.reduce((sum, p) => sum + Number(p.longitude), 0) / zoneDrawPoints.length;

  document.getElementById("zoneLat").value = avgLat.toFixed(6);
  document.getElementById("zoneLon").value = avgLon.toFixed(6);
}

function startObjectivePlacement() {
  mapClickMode = "objective";
  updateMapHelp("Click the map where this objective should appear. You can drag the marker after placing it.");
}

function setObjectiveDraftLocation(latlng) {
  document.getElementById("objectiveLat").value = latlng.lat.toFixed(6);
  document.getElementById("objectiveLon").value = latlng.lng.toFixed(6);

  if (!objectiveDraftMarker) {
    objectiveDraftMarker = L.marker(latlng, { draggable: true, icon: createDraftObjectiveIcon() }).addTo(draftLayer);
    objectiveDraftMarker.bindPopup("Draft objective location");
    objectiveDraftMarker.on("drag", event => {
      const markerLatLng = event.target.getLatLng();
      document.getElementById("objectiveLat").value = markerLatLng.lat.toFixed(6);
      document.getElementById("objectiveLon").value = markerLatLng.lng.toFixed(6);
    });
  } else {
    objectiveDraftMarker.setLatLng(latlng);
  }
}

async function startGameAndOpenScoreboard() {
  if (startGameRequestInFlight) {
    return;
  }

  const startButton = document.getElementById("actualStartGameButton");
  startGameRequestInFlight = true;
  if (startButton) {
    startButton.disabled = true;
    startButton.dataset.originalText = startButton.textContent || "Start Game";
    startButton.textContent = "Starting...";
  }

  try {
    const result = await postJson(`/api/games/${GAME_ID}/start`);
    showMessage(result.message || "Game started.", "success");
    suppressAutoScoreboardRedirectUntil = Date.now() + 5000;
    window.location.href = `/scoreboard/45D.3?ts=${Date.now()}`;
  } catch (error) {
    showMessage(error.message, "error");
    startGameRequestInFlight = false;
    if (startButton) {
      startButton.disabled = false;
      startButton.textContent = startButton.dataset.originalText || "Start Game";
    }
  }
}

async function postGameAction(action) {
  try {
    const result = await postJson(`/api/games/${GAME_ID}/${action}`);
    showMessage(result.message || `${action} complete.`, "success");
    await loadAdminData();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function resetGame() {
  const confirmed = await showAdminConfirm({
    title: "Reset local game?",
    message: "This clears the current in-memory game state.",
    confirmText: "Reset Game",
    cancelText: "Keep Game",
    danger: true
  });

  if (!confirmed) {
    return;
  }

  try {
    const result = await postJson(`/api/games/${GAME_ID}/reset`);
    clearZoneDraft();
    if (objectiveDraftMarker) {
      draftLayer.removeLayer(objectiveDraftMarker);
      objectiveDraftMarker = null;
    }
    showMessage(result.message || "Game reset.", "success");
    await loadAdminData();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function setGameMode() {
  try {
    const selectedMode = getSelectedGameModeDefinition();
    renderGameModeSettingsSection();
    updateProximityTaggingAvailability();
    if (selectedMode && selectedMode.available === false) {
      throw new Error(`${selectedMode.displayName} is not available: ${selectedMode.unavailableReason || "disabled in Admin"}`);
    }
    if (selectedMode && !canHostGameMode(selectedMode.modeId)) {
      throw new Error(`${selectedMode.displayName} requires DLC ownership for the host.`);
    }

    const body = {
      rulesFile: document.getElementById("gameModeSelect").value,
      clearObjectives: false,
      clearEvents: false,
      resetScores: false
    };

    const result = await postJson(`/api/games/${GAME_ID}/mode`, body);
    showMessage(result.message || "Game mode saved.", "success");
    await loadAdminData();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function runGameTick() {
  try {
    const result = await postJson(`/api/games/${GAME_ID}/tick`);
    showMessage(`${result.message} Active objectives: ${result.activeObjectives}`, "success");
    await loadAdminData();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function addZone() {
  try {
    const boundaryPoints = zoneDrawPoints.map(p => ({
      latitude: Number(p.latitude),
      longitude: Number(p.longitude)
    }));

    if (boundaryPoints.length < 3) {
      throw new Error("Draw at least 3 boundary points before activating the play zone.");
    }

    const body = {
      name: document.getElementById("zoneName").value.trim() || "Play Zone",
      boundaryPoints,
      centerLatitude: null,
      centerLongitude: null,
      radiusMeters: 0,
      allowDynamicObjectives: document.getElementById("zoneAllowDynamic").checked
    };

    const result = await postJson(`/api/games/${GAME_ID}/zones`, body);

    currentZone = result.zone;

    showMessage(result.message || "Play zone activated.", "success");
    clearZoneDraft();
    await loadAdminData();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

function editExistingZone() {
  if (!currentZone || !currentZone.boundaryPoints || currentZone.boundaryPoints.length < 3) {
    showMessage("No play zone exists yet. Draw one first.", "error");
    return;
  }

  clearZoneDraft();

  zoneDrawPoints = currentZone.boundaryPoints.map(point => ({
    latitude: Number(point.latitude),
    longitude: Number(point.longitude)
  }));

  zoneDrawPoints.forEach((point, index) => {
    const latlng = L.latLng(point.latitude, point.longitude);
    const marker = L.marker(latlng, {
      draggable: true,
      icon: createVertexIcon(index + 1)
    }).addTo(draftLayer);

    marker.on("drag", event => {
      const markerLatLng = event.target.getLatLng();

      zoneDrawPoints[index] = {
        latitude: markerLatLng.lat,
        longitude: markerLatLng.lng
      };

      updateZoneDraftPolygon();
      updateZoneFallbackCenterFromDraft();
    });

    marker.on("dragend", event => {
      const markerLatLng = event.target.getLatLng();

      zoneDrawPoints[index] = {
        latitude: markerLatLng.lat,
        longitude: markerLatLng.lng
      };

      updateZoneDraftPolygon();
      updateZoneFallbackCenterFromDraft();
    });

    zoneVertexMarkers.push(marker);
  });

  updateZoneDraftPolygon();
  updateZoneFallbackCenterFromDraft();

  mapClickMode = "zone";
  updateMapHelp("Editing play zone. Drag points to adjust, click map to add more points, then click Activate Play Zone.");
  showMessage("Editing existing play zone.", "success");
}

async function addObjective() {
  try {
    const body = {
      name: requiredText("objectiveName", "Objective name is required."),
      objectiveType: requiredText("objectiveType", "Objective type is required."),
      latitude: requiredNumber("objectiveLat", "Objective latitude is required."),
      longitude: requiredNumber("objectiveLon", "Objective longitude is required."),
      radiusMeters: requiredNumber("objectiveRadius", "Objective radius is required."),
      pointValue: requiredNumber("objectivePoints", "Objective point value is required."),
      holdSecondsRequired: requiredNumber("objectiveHoldSeconds", "Objective hold seconds is required."),
      claimLimit: document.getElementById("objectiveClaimLimit").value,
      allowStealing: document.getElementById("objectiveAllowStealing").checked,
      requireHoldToCapture: document.getElementById("objectiveRequireHold").checked,
      removeAfterClaimed: document.getElementById("objectiveRemoveAfterClaimed").checked,
      visibility: "Public"
    };

    const result = await postJson(`/api/games/${GAME_ID}/objectives`, body);
    showMessage(result.message || "Objective added.", "success");
    await loadAdminData();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function deactivateObjective(objectiveId) {
  try {
    const result = await postJson(`/api/games/${GAME_ID}/objectives/${objectiveId}/deactivate`);
    showMessage(result.message || "Objective deactivated.", "success");
    await loadAdminData();
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function centerMapOnCurrentLocation() {
  if (!adminMap) {
    showMessage("Map is not available yet.", "error");
    return;
  }
  try {
    const location = await getCurrentLocation();
    const latLng = [location.latitude, location.longitude];
    adminMap.setView(latLng, 17);
    L.marker(latLng, { icon: createYouAreHereIcon() }).addTo(draftLayer).bindPopup(`Your location. Accuracy: ${Math.round(location.accuracy)}m`).openPopup();
    showMessage(`Map centered. Accuracy: ${Math.round(location.accuracy)}m`, "success");
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function searchAddress() {
  if (!adminMap) {
    showMessage("Map is not available yet.", "error");
    return;
  }
  const query = document.getElementById("addressSearch").value.trim();
  if (!query) {
    showMessage("Enter an address or place to search.", "error");
    return;
  }

  try {
    showMessage("Searching address...", "");
    const url = `https://nominatim.openstreetmap.org/search?format=json&limit=1&addressdetails=1&q=${encodeURIComponent(query)}`;
    const response = await fetch(url, {
      headers: {
        "Accept": "application/json"
      }
    });

    const data = await response.json();
    if (!response.ok || !Array.isArray(data) || data.length === 0) {
      throw new Error("Address not found.");
    }

    const result = data[0];
    const lat = Number(result.lat);
    const lon = Number(result.lon);

    if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
      throw new Error("Address result did not include a valid location.");
    }

    adminMap.setView([lat, lon], 16);
    document.getElementById("zoneLat").value = lat.toFixed(6);
    document.getElementById("zoneLon").value = lon.toFixed(6);
    showMessage(`Map centered on: ${result.display_name}`, "success");
  } catch (error) {
    showMessage(error.message, "error");
  }
}

async function fillCurrentLocation(latInputId, lonInputId, centerMap = false) {
  try {
    const location = await getCurrentLocation();
    document.getElementById(latInputId).value = location.latitude.toFixed(6);
    document.getElementById(lonInputId).value = location.longitude.toFixed(6);

    if (centerMap && adminMap) {
      adminMap.setView([location.latitude, location.longitude], 17);
    }

    showMessage(`Location filled. Accuracy: ${Math.round(location.accuracy)}m`, "success");
  } catch (error) {
    showMessage(error.message, "error");
  }
}

function createVertexIcon(number) {
  return L.divIcon({
    className: "map-div-icon",
    html: `<div class="map-vertex-pin">${number}</div>`,
    iconSize: [26, 26],
    iconAnchor: [13, 13]
  });
}

function createObjectiveIcon(objective = null) {
  const isCtf = objective && isCtfObjective(objective);
  const objectiveType = (objective?.objectiveType || "").toLowerCase();
  const objectiveId = (objective?.objectiveId || "").toLowerCase();
  const isFlag = objectiveType === CTF_FLAG_TYPE || objectiveId.startsWith("ctf_flag_");
  const isBase = objectiveType === CTF_BASE_TYPE || objectiveId.startsWith("ctf_base_");
  const label = isFlag ? "⚑" : isBase ? "B" : "";
  const teamClass = getCtfObjectiveTeamColorClass(objective);
  const teamColor = isCtf ? getObjectiveTeamColor(objective) : "";
  const teamStyle = teamColor ? ` style="--team-color:${escapeHtml(teamColor)};"` : "";
  const className = isFlag
    ? `map-objective-pin map-ctf-flag-pin map-team-colored ${teamClass}`.trim()
    : isBase
      ? `map-objective-pin map-ctf-base-pin map-team-colored ${teamClass}`.trim()
      : "map-objective-pin";

  return L.divIcon({
    className: "map-div-icon",
    html: `<div class="${className}"${teamStyle}>${isCtf ? label : ""}</div>`,
    iconSize: isCtf ? [32, 32] : [26, 26],
    iconAnchor: isCtf ? [16, 16] : [13, 13]
  });
}

function createDraftObjectiveIcon() {
  return L.divIcon({
    className: "map-div-icon",
    html: `<div class="map-draft-objective-pin"></div>`,
    iconSize: [30, 30],
    iconAnchor: [15, 15]
  });
}

function createYouAreHereIcon() {
  return L.divIcon({
    className: "map-div-icon",
    html: `<div class="map-you-pin"></div>`,
    iconSize: [28, 28],
    iconAnchor: [14, 14]
  });
}

function updateMapHelp(message) {
  const el = document.getElementById("mapHelp");
  if (!el) {
    return;
  }

  el.textContent = message;
}

function requiredText(id, message) {
  const value = document.getElementById(id).value.trim();
  if (!value) throw new Error(message);
  return value;
}

function requiredNumber(id, message) {
  const value = Number(document.getElementById(id).value);
  if (!Number.isFinite(value)) throw new Error(message);
  return value;
}

function getCurrentLocation() {
  return new Promise((resolve, reject) => {
    if (!navigator.geolocation) {
      reject(new Error("This browser does not support location."));
      return;
    }

    navigator.geolocation.getCurrentPosition(
      position => resolve({
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
        accuracy: position.coords.accuracy
      }),
      error => reject(new Error(getLocationErrorMessage(error))),
      { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
    );
  });
}

function getLocationErrorMessage(error) {
  switch (error.code) {
    case error.PERMISSION_DENIED:
      return "Location permission was denied. Use address search instead.";
    case error.POSITION_UNAVAILABLE:
      return "Location is currently unavailable. Use address search instead.";
    case error.TIMEOUT:
      return "Location request timed out. Use address search instead.";
    default:
      return "Could not get location. Use address search instead.";
  }
}


async function getAuthHeaders(extraHeaders = {}) {
  const headers = { ...extraHeaders };
  const idToken = await window.adminAuth?.getIdToken?.();

  if (idToken) {
    headers.Authorization = `Bearer ${idToken}`;
  }

  return headers;
}

async function getJson(url) {
  const response = await fetch(url, {
    headers: await getAuthHeaders()
  });
  const data = await readResponse(response);
  if (!response.ok) throw new Error(getErrorMessage(data, response));
  return data;
}

async function postJson(url, body = {}) {
  const response = await fetch(url, {
    method: "POST",
    headers: await getAuthHeaders({ "Content-Type": "application/json" }),
    body: JSON.stringify(body)
  });

  const data = await readResponse(response);
  if (!response.ok) throw new Error(getErrorMessage(data, response));
  return data;
}

async function putJson(url, body) {
  const response = await fetch(url, {
    method: "PUT",
    headers: await getAuthHeaders({
      "Content-Type": "application/json"
    }),
    body: JSON.stringify(body)
  });

  const data = await response.json().catch(() => ({}));

  if (!response.ok) {
    throw new Error(data.error || data.message || `Request failed: ${response.status}`);
  }

  return data;
}

async function deleteJson(url) {
  const response = await fetch(url, {
    method: "DELETE",
    headers: await getAuthHeaders()
  });

  const data = await readResponse(response);
  if (!response.ok) throw new Error(getErrorMessage(data, response));
  return data;
}

async function readResponse(response) {
  const text = await response.text();
  try {
    return text ? JSON.parse(text) : {};
  } catch {
    return { raw: text };
  }
}

function getErrorMessage(data, response) {
  return data?.error || data?.message || data?.title || data?.detail || data?.raw || `Request failed with status ${response.status}.`;
}

function showMessage(message, type = "", keepVisible = true) {
  const el = document.getElementById("adminMessage");
  if (!el) return;

  el.textContent = message;
  el.className = type ? `status ${type}` : "status";
  el.style.display = keepVisible ? "block" : "none";
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

async function cancelReadyLobby() {
  const confirmed = await showAdminConfirm({
    title: "Cancel join lobby?",
    message: "This closes the current lobby, removes joined players, and unlocks setup settings.",
    confirmText: "Cancel Lobby",
    cancelText: "Keep Lobby",
    danger: true
  });

  if (!confirmed) {
    return;
  }

  try {
    const result = await postJson(`/api/games/${GAME_ID}/cancel-ready`);
    clearAdminFormDirty();
    showMessage(result.message || "Ready lobby cancelled.", "success");
    await loadAdminData();

    activateAdminTab("gameSettings");
  } catch (error) {
    showMessage(error.message, "error");
    await loadAdminDataQuietly();
  }
}

async function enterJoinLobby() {
  const blockers = getReadyBlockers();

  if (blockers.length > 0) {
    showMessage(blockers[0].message, "error");
    renderReadyRequirements();
    return;
  }

  try {
    const result = await postJson(`/api/games/${GAME_ID}/ready`);
    clearAdminFormDirty();
    showMessage(result.message || "Server is ready. Players can join.", "success");
    await loadAdminData();

    activateAdminTab("joinLobby");
  } catch (error) {
    showMessage(error.message, "error");
    await loadAdminDataQuietly();
  }
}

function activateAdminTab(tabName) {
  activeAdminTab = tabName;

  if (tabName === "joinLobby") {
    clearAdminFormDirty();
    if (isAdminEditableElement(document.activeElement)) {
      document.activeElement.blur();
    }

    // Give the lobby one immediate local refresh after tab activation so a
    // player who joins right after Ready/Cancel does not wait for the next
    // background tick. This reads the local server API, not Firestore.
    window.setTimeout(() => {
      refreshLiveAdminDataQuietly();
    }, 150);
  }

  document.querySelectorAll("[data-admin-tab]").forEach(tabButton => {
    tabButton.classList.toggle("active", tabButton.dataset.adminTab === tabName);
  });

  document.querySelectorAll(".admin-tab-panel").forEach(panel => {
    panel.classList.toggle("active", panel.id === `tab-${tabName}`);
  });

  if (tabName === "playZone" && adminMap) {
    window.setTimeout(() => adminMap.invalidateSize(), 80);
    window.setTimeout(() => renderMapData({ preserveMapView: true }), 100);
  }
}