import * as MenuConfig from './menu-config.js';

const SETTINGS_KEY = 'q4_settings_v1';
const LAUNCHER_KEY = 'q4_launcher_v1';
const VIEWS = new Set(['campaign', 'multiplayer', 'replays', 'settings']);
const TEAM_MODES = new Set(['Team DM', 'CTF', 'Arena CTF']);

export function missingBundleBytes(selection, installedBundleIds = []) {
  const bundles = Array.isArray(selection?.bundles) ? selection.bundles : [];
  if (bundles.length === 0) {
    return Number.isSafeInteger(selection?.totalBytes) && selection.totalBytes > 0
      ? selection.totalBytes
      : 0;
  }
  const installed = new Set(Array.isArray(installedBundleIds) ? installedBundleIds : []);
  return bundles.reduce((total, bundle) => (
    total + (installed.has(bundle.id) ? 0 : bundle.size)
  ), 0);
}

function clone(value) {
  if (typeof structuredClone === 'function') return structuredClone(value);
  return JSON.parse(JSON.stringify(value));
}

function readJson(storage, key) {
  try {
    const value = storage?.getItem?.(key);
    return value ? JSON.parse(value) : null;
  } catch {
    return null;
  }
}

function writeJson(storage, key, value) {
  try {
    storage?.setItem?.(key, JSON.stringify(value));
  } catch {
    // A private browsing quota error must not prevent starting the game.
  }
}

function itemArray(source) {
  if (Array.isArray(source)) return source.map((item, index) => ({
    key: String(item?.id ?? item?.value ?? item?.mode ?? item?.map ?? index),
    value: item
  }));
  if (!source || typeof source !== 'object') return [];
  return Object.entries(source).map(([key, value]) => ({ key, value }));
}

function itemId(entry, fields) {
  const item = entry?.value;
  if (item && typeof item === 'object') {
    for (const field of fields) {
      if (item[field] !== undefined && item[field] !== null) return String(item[field]);
    }
  }
  if (typeof item === 'string' || typeof item === 'number') return String(item);
  return String(entry?.key ?? '');
}

function itemLabel(entry, fallback) {
  const item = entry?.value;
  if (item && typeof item === 'object') {
    return String(item.label ?? item.name ?? item.title ?? item.displayName ?? fallback);
  }
  if (typeof item === 'string') return item;
  return String(fallback);
}

function normalizeDifficulties(source) {
  return itemArray(source).map((entry, index) => {
    const raw = entry.value;
    const value = raw && typeof raw === 'object'
      ? raw.value ?? raw.skill ?? raw.id ?? index
      : raw ?? index;
    return {
      value,
      id: String(value),
      label: itemLabel(entry, `Poziom ${index + 1}`),
      description: raw && typeof raw === 'object'
        ? String(raw.description ?? raw.subtitle ?? '')
        : ''
    };
  });
}

function normalizeModes(source) {
  return itemArray(source).map((entry) => {
    const raw = entry.value;
    return {
      id: itemId(entry, ['id', 'value', 'mode', 'gameType']),
      label: itemLabel(entry, itemId(entry, ['id', 'value', 'mode', 'gameType'])),
      description: raw && typeof raw === 'object'
        ? String(raw.description ?? raw.subtitle ?? '')
        : ''
    };
  }).filter((item) => item.id);
}

function normalizeMaps(source) {
  return itemArray(source).map((entry) => {
    const raw = entry.value;
    const modes = raw && typeof raw === 'object'
      ? raw.modes ?? raw.supportedModes ?? raw.gameTypes ?? []
      : [];
    const id = itemId(entry, ['id', 'value', 'map', 'path']);
    const normalizedModes = Array.isArray(modes) ? modes.map(String) : [];
    const declaredBotModes = raw && typeof raw === 'object' && Array.isArray(raw.botModes)
      ? [...new Set(raw.botModes.map(String).filter((mode) => normalizedModes.includes(mode)))]
      : [];
    const legacyBotReady = Boolean(raw && typeof raw === 'object' && (raw.botReady ?? raw.bots));
    const botNavigationReady = Boolean(raw && typeof raw === 'object'
      ? raw.botNavigationReady ?? raw.navigationReady ?? legacyBotReady
      : false);
    const botModes = botNavigationReady
      ? (Array.isArray(raw?.botModes)
        ? declaredBotModes
        : normalizedModes.includes('DM') && legacyBotReady ? ['DM'] : [])
      : [];
    return {
      id,
      label: itemLabel(entry, id),
      code: raw && typeof raw === 'object' ? String(raw.code ?? id.split('/').pop() ?? id) : id,
      modes: normalizedModes,
      botNavigationReady,
      botModes,
      botReady: botModes.length > 0,
      maxBots: Number(raw && typeof raw === 'object' ? raw.maxBots ?? 8 : 0) || 0
    };
  }).filter((item) => item.id);
}

function normalizeTeams(source) {
  const teams = itemArray(source).map((entry) => ({
    id: itemId(entry, ['id', 'value', 'team']),
    label: itemLabel(entry, itemId(entry, ['id', 'value', 'team']))
  })).filter((item) => item.id);
  if (!teams.some((team) => team.id === 'auto')) teams.unshift({ id: 'auto', label: 'Automatycznie' });
  return teams;
}

function normalizeModels(source) {
  const models = itemArray(source).map((entry) => {
    const raw = entry.value;
    const id = itemId(entry, ['id', 'value', 'model']);
    return {
      id,
      label: itemLabel(entry, id),
      team: raw && typeof raw === 'object' && raw.team ? String(raw.team) : ''
    };
  }).filter((item) => item.id && item.id !== 'auto');
  return [{ id: 'auto', label: 'Automatycznie', team: '' }, ...models];
}

function numberOr(value, fallback) {
  const number = Number(value);
  return Number.isFinite(number) ? number : fallback;
}

function matchMode(match) {
  return String(match?.mode ?? match?.gameType ?? '');
}

function matchMap(match) {
  return String(match?.map ?? match?.mapId ?? '');
}

function normalizedSettings(input) {
  try {
    return MenuConfig.normalizeSettings(input ?? MenuConfig.DEFAULT_SETTINGS ?? {});
  } catch {
    return { ...(MenuConfig.DEFAULT_SETTINGS ?? {}), ...(input ?? {}) };
  }
}

function normalizedDifficulty(input, difficulties) {
  try {
    return MenuConfig.normalizeDifficulty(input);
  } catch {
    return difficulties[0]?.value ?? 1;
  }
}

function normalizedMatch(input) {
  const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
  const canonical = {};
  const map = source.map ?? source.mapId;
  const mode = source.mode ?? source.gameType;
  const botCount = source.botCount ?? source.bots;
  if (map !== undefined) canonical.map = map;
  if (mode !== undefined) canonical.mode = mode;
  if (botCount !== undefined) canonical.botCount = botCount;
  if (source.fragLimit !== undefined) canonical.fragLimit = source.fragLimit;
  if (source.timeLimit !== undefined) canonical.timeLimit = source.timeLimit;
  try {
    return MenuConfig.normalizeMatchRequest(canonical);
  } catch {
    return { ...(MenuConfig.DEFAULT_MATCH ?? {}) };
  }
}

function normalizedProfile(input, models) {
  const allowed = models.filter((model) => model.id !== 'auto').map((model) => model.id);
  try {
    return MenuConfig.normalizePlayerProfile(input ?? MenuConfig.DEFAULT_PLAYER_PROFILE ?? {}, allowed);
  } catch {
    return { ...(MenuConfig.DEFAULT_PLAYER_PROFILE ?? {}), ...(input ?? {}) };
  }
}

function bytesLabel(value) {
  const bytes = Number(value);
  if (!Number.isFinite(bytes) || bytes < 0) return '';
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
}

function dateLabel(value) {
  const date = new Date(value);
  if (!Number.isFinite(date.getTime())) return '';
  return date.toLocaleString('pl-PL', { dateStyle: 'short', timeStyle: 'short' });
}

function replayMap(replay) {
  return String(replay?.metadata?.map ?? replay?.map ?? '').trim();
}

function durationLabel(replay) {
  const durationMs = Number(replay?.metadata?.durationMs);
  if (Number.isFinite(durationMs) && durationMs >= 0) {
    const totalSeconds = Math.floor(durationMs / 1000);
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;
    return hours > 0
      ? `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
      : `${minutes}:${String(seconds).padStart(2, '0')}`;
  }
  return replay?.duration ? String(replay.duration) : '';
}

function replayId(replay) {
  return String(replay?.id ?? replay?.name ?? '');
}

function requiredElement(documentObject, id) {
  const element = documentObject.getElementById(id);
  if (!element) throw new Error(`Brak elementu launchera: #${id}`);
  return element;
}

export function createLauncher(options = {}) {
  const documentObject = options.document ?? globalThis.document;
  if (!documentObject) throw new Error('Launcher wymaga dokumentu HTML.');

  const storage = options.storage ?? globalThis.localStorage;
  const root = options.root ?? requiredElement(documentObject, 'launcher');
  const body = documentObject.body;
  const difficulties = normalizeDifficulties(MenuConfig.DIFFICULTIES);
  let modes = normalizeModes(MenuConfig.MULTIPLAYER_MODES);
  let maps = normalizeMaps(MenuConfig.MULTIPLAYER_MAPS);
  let teams = normalizeTeams(MenuConfig.PLAYER_TEAMS);
  let models = normalizeModels([]);
  let replays = [];
  let selectedReplay = null;
  let busy = false;
  let status = null;

  const persistedLauncher = readJson(storage, LAUNCHER_KEY) ?? {};
  const persistedSettings = readJson(storage, SETTINGS_KEY);
  const persistedView = VIEWS.has(persistedLauncher.activeView) ? persistedLauncher.activeView : 'campaign';
  let deferredModel = String(persistedLauncher?.profile?.model ?? '');
  let candidate = {
    ...(MenuConfig.DEFAULT_LAUNCHER ?? {}),
    ...persistedLauncher,
    settings: persistedSettings ?? persistedLauncher.settings ?? MenuConfig.DEFAULT_SETTINGS
  };
  try {
    candidate = MenuConfig.normalizeLauncherState(candidate, []);
  } catch {
    // Individual normalizers below retain a usable menu when storage is stale.
  }

  const state = {
    activeView: persistedView,
    difficulty: normalizedDifficulty(candidate.difficulty, difficulties),
    match: normalizedMatch(candidate.match),
    profile: normalizedProfile(candidate.profile, models),
    settings: normalizedSettings(candidate.settings)
  };

  const elements = {
    announcement: requiredElement(documentObject, 'launcherAnnouncement'),
    difficulty: requiredElement(documentObject, 'campaignDifficulty'),
    campaignSummary: requiredElement(documentObject, 'campaignSummary'),
    campaignPlay: requiredElement(documentObject, 'campaignPlayButton'),
    modes: requiredElement(documentObject, 'multiplayerModes'),
    maps: requiredElement(documentObject, 'multiplayerMaps'),
    mapCount: requiredElement(documentObject, 'multiplayerMapCount'),
    teams: requiredElement(documentObject, 'multiplayerTeams'),
    model: requiredElement(documentObject, 'multiplayerModel'),
    bots: requiredElement(documentObject, 'multiplayerBots'),
    botsValue: requiredElement(documentObject, 'multiplayerBotsValue'),
    botsHint: requiredElement(documentObject, 'multiplayerBotsHint'),
    fragLimit: requiredElement(documentObject, 'multiplayerFragLimit'),
    fragLimitValue: requiredElement(documentObject, 'multiplayerFragLimitValue'),
    timeLimit: requiredElement(documentObject, 'multiplayerTimeLimit'),
    timeLimitValue: requiredElement(documentObject, 'multiplayerTimeLimitValue'),
    multiplayerSummary: requiredElement(documentObject, 'multiplayerSummary'),
    multiplayerPlay: requiredElement(documentObject, 'multiplayerPlayButton'),
    serverBrief: requiredElement(documentObject, 'launcherServerBrief'),
    serverStatus: requiredElement(documentObject, 'launcherServerStatus'),
    serverStatusText: requiredElement(documentObject, 'launcherServerStatusText'),
    serverPlayers: requiredElement(documentObject, 'launcherServerPlayers'),
    replaySearch: requiredElement(documentObject, 'replaySearch'),
    replayRefresh: requiredElement(documentObject, 'replayRefreshButton'),
    replayList: requiredElement(documentObject, 'launcherReplayList'),
    replayCount: requiredElement(documentObject, 'launcherReplayCount'),
    replayDetails: requiredElement(documentObject, 'launcherReplayDetails'),
    replaySummary: requiredElement(documentObject, 'replaySummary'),
    replayPlay: requiredElement(documentObject, 'replayPlayButton'),
    sensitivity: requiredElement(documentObject, 'settingSensitivity'),
    sensitivityValue: requiredElement(documentObject, 'settingSensitivityValue'),
    invertY: requiredElement(documentObject, 'settingInvertY'),
    fov: requiredElement(documentObject, 'settingFov'),
    fovValue: requiredElement(documentObject, 'settingFovValue'),
    masterVolume: requiredElement(documentObject, 'settingMasterVolume'),
    masterVolumeValue: requiredElement(documentObject, 'settingMasterVolumeValue'),
    musicVolume: requiredElement(documentObject, 'settingMusicVolume'),
    musicVolumeValue: requiredElement(documentObject, 'settingMusicVolumeValue'),
    settingsSaved: requiredElement(documentObject, 'settingsSavedState')
  };

  function persist() {
    const launcherState = {
      activeView: state.activeView,
      difficulty: state.difficulty,
      match: state.match,
      profile: state.profile
    };
    writeJson(storage, SETTINGS_KEY, state.settings);
    writeJson(storage, LAUNCHER_KEY, launcherState);
    elements.settingsSaved.textContent = 'Zapisano lokalnie';
  }

  function selectedMode() {
    return matchMode(state.match) || modes[0]?.id || '';
  }

  function selectedMap() {
    return matchMap(state.match) || maps[0]?.id || '';
  }

  function isTeamMode() {
    return TEAM_MODES.has(selectedMode());
  }

  function reconcileProfileForMode() {
    if (!isTeamMode()) return false;
    const model = models.find((entry) => entry.id === state.profile.model);
    let team = state.profile?.team ?? 'auto';
    let modelId = state.profile.model;
    if (modelId !== 'auto') {
      if (model?.team) team = model.team;
      else modelId = 'auto';
    }
    if (team === 'auto') team = 'Marine';
    if (team === state.profile?.team && modelId === state.profile?.model) return false;
    state.profile = normalizedProfile({ ...state.profile, team, model: modelId }, models);
    persist();
    return true;
  }

  function replaceMatch(patch) {
    const mode = String(patch.mode ?? patch.gameType ?? selectedMode());
    const requestedMap = String(patch.map ?? patch.mapId ?? selectedMap());
    const map = maps.find((entry) => entry.id === requestedMap);
    const compatibleMap = map && (map.modes.length === 0 || map.modes.includes(mode))
      ? map
      : maps.find((entry) => entry.modes.length === 0 || entry.modes.includes(mode));
    const scoreLimit = ['CTF', 'Arena CTF'].includes(mode) && Number(state.match?.fragLimit) < 1
      ? 1
      : state.match?.fragLimit;
    state.match = normalizedMatch({
      ...state.match,
      fragLimit: scoreLimit,
      ...patch,
      mode,
      gameType: mode,
      ...(compatibleMap ? { map: compatibleMap.id, mapId: compatibleMap.id } : {})
    });
    persist();
  }

  function renderDifficulty() {
    elements.difficulty.replaceChildren();
    for (const difficulty of difficulties) {
      const button = documentObject.createElement('button');
      button.type = 'button';
      button.role = 'radio';
      button.dataset.difficulty = difficulty.id;
      button.setAttribute('aria-checked', String(String(state.difficulty) === difficulty.id));
      const label = documentObject.createElement('span');
      label.textContent = difficulty.label;
      button.append(label);
      if (difficulty.description) {
        const description = documentObject.createElement('small');
        description.textContent = difficulty.description;
        button.append(description);
      }
      elements.difficulty.append(button);
    }
    const current = difficulties.find((item) => String(item.value) === String(state.difficulty));
    elements.campaignSummary.textContent = current ? `Nowa kampania · ${current.label}` : 'Nowa kampania';
  }

  function renderModes() {
    elements.modes.replaceChildren();
    const current = selectedMode();
    for (const mode of modes) {
      const button = documentObject.createElement('button');
      button.type = 'button';
      button.role = 'radio';
      button.dataset.mode = mode.id;
      button.setAttribute('aria-checked', String(mode.id === current));
      const label = documentObject.createElement('span');
      label.textContent = mode.label;
      button.append(label);
      if (mode.description) {
        const description = documentObject.createElement('small');
        description.textContent = mode.description;
        button.append(description);
      }
      elements.modes.append(button);
    }
  }

  function compatibleMaps() {
    const mode = selectedMode();
    return maps.filter((map) => map.modes.length === 0 || map.modes.includes(mode));
  }

  function ensureCompatibleMap() {
    const compatible = compatibleMaps();
    if (!compatible.some((map) => map.id === selectedMap()) && compatible[0]) {
      replaceMatch({ map: compatible[0].id, mapId: compatible[0].id });
    }
  }

  function renderMaps() {
    ensureCompatibleMap();
    const compatible = compatibleMaps();
    const current = selectedMap();
    elements.maps.replaceChildren();
    for (const map of compatible) {
      const button = documentObject.createElement('button');
      button.type = 'button';
      button.role = 'radio';
      button.className = 'launcher-map';
      button.dataset.map = map.id;
      button.dataset.code = map.code;
      button.setAttribute('aria-checked', String(map.id === current));
      const name = documentObject.createElement('strong');
      name.textContent = map.label;
      const meta = documentObject.createElement('span');
      meta.textContent = map.botModes.includes(selectedMode()) ? 'Boty dostępne' : 'Gra sieciowa';
      button.append(name, meta);
      elements.maps.append(button);
    }
    elements.mapCount.textContent = String(compatible.length);
    renderMatchControls();
  }

  function renderTeams() {
    const current = String(state.profile?.team ?? 'auto');
    elements.teams.replaceChildren();
    const visibleTeams = isTeamMode() ? teams.filter((team) => team.id !== 'auto') : teams;
    for (const team of visibleTeams) {
      const button = documentObject.createElement('button');
      button.type = 'button';
      button.role = 'radio';
      button.dataset.team = team.id;
      button.setAttribute('aria-checked', String(team.id === current));
      button.textContent = team.label;
      elements.teams.append(button);
    }
  }

  function visibleModels() {
    const team = String(state.profile?.team ?? 'auto');
    if (!isTeamMode()) return models;
    return models.filter((model) => model.id === 'auto' || Boolean(model.team) &&
      (team === 'auto' || model.team === team));
  }

  function renderModels() {
    const visible = visibleModels();
    let current = String(state.profile?.model ?? 'auto');
    if (!visible.some((model) => model.id === current)) current = 'auto';
    if (current !== String(state.profile?.model ?? 'auto')) {
      state.profile = normalizedProfile({ ...state.profile, model: current }, models);
      persist();
    }
    elements.model.replaceChildren();
    for (const model of visible) {
      const option = documentObject.createElement('option');
      option.value = model.id;
      option.textContent = model.label;
      elements.model.append(option);
    }
    elements.model.value = current;
  }

  function renderMatchControls() {
    const map = maps.find((item) => item.id === selectedMap());
    const botReady = Boolean(map?.botModes.includes(selectedMode()));
    const botMax = botReady ? Math.max(0, map.maxBots || 8) : 0;
    let bots = numberOr(state.match?.bots ?? state.match?.botCount, 0);
    bots = Math.max(0, Math.min(botMax, Math.round(bots)));
    if (bots !== numberOr(state.match?.bots ?? state.match?.botCount, 0)) {
      replaceMatch({ bots, botCount: bots });
    }
    elements.bots.max = String(botMax);
    elements.bots.disabled = !botReady;
    elements.bots.value = String(bots);
    elements.botsValue.textContent = String(bots);
    elements.botsHint.textContent = botReady
      ? ''
      : map?.botNavigationReady
        ? 'Boty nie są jeszcze dostępne w tym trybie.'
        : 'Ta mapa nie ma nawigacji dla botów.';

    const fragLimit = numberOr(state.match?.fragLimit, 20);
    const timeLimit = numberOr(state.match?.timeLimit, 10);
    const captureMode = ['CTF', 'Arena CTF'].includes(selectedMode());
    elements.fragLimit.min = captureMode ? '1' : '0';
    elements.fragLimit.step = captureMode ? '1' : '5';
    elements.fragLimit.value = String(fragLimit);
    elements.fragLimitValue.textContent = fragLimit === 0 ? 'Bez limitu' : String(fragLimit);
    const scoreLabel = elements.fragLimit.closest('label')?.querySelector('b');
    if (scoreLabel) scoreLabel.textContent = captureMode
      ? 'Limit przejęć'
      : 'Limit fragów';
    elements.timeLimit.value = String(timeLimit);
    elements.timeLimitValue.textContent = timeLimit === 0 ? 'Bez limitu' : `${timeLimit} min`;

    const mode = modes.find((item) => item.id === selectedMode());
    elements.multiplayerSummary.textContent = [map?.label, mode?.label].filter(Boolean).join(' · ') || 'Wybierz mapę';
  }

  function renderSettings() {
    const settings = state.settings;
    const sensitivity = numberOr(settings.sensitivity, 5);
    const fov = numberOr(settings.fov, 90);
    const master = numberOr(settings.masterVolume ?? settings.volume, 0.8);
    const music = numberOr(settings.musicVolume, 0.55);
    const masterPercent = master <= 1 ? Math.round(master * 100) : Math.round(master);
    const musicPercent = music <= 1 ? Math.round(music * 100) : Math.round(music);

    elements.sensitivity.value = String(sensitivity);
    elements.sensitivityValue.textContent = String(sensitivity);
    elements.invertY.checked = Boolean(settings.invertY);
    elements.fov.value = String(fov);
    elements.fovValue.textContent = `${Math.round(fov)}°`;
    elements.masterVolume.value = String(masterPercent);
    elements.masterVolumeValue.textContent = `${masterPercent}%`;
    elements.musicVolume.value = String(musicPercent);
    elements.musicVolumeValue.textContent = `${musicPercent}%`;
  }

  function replayMeta(replay) {
    const parts = [dateLabel(replay?.modifiedAt ?? replay?.createdAt), bytesLabel(replay?.size)];
    const map = replayMap(replay);
    const gameType = String(replay?.metadata?.gameType ?? '').trim();
    if (gameType) parts.unshift(gameType);
    if (map) parts.unshift(map);
    return parts.filter(Boolean).join(' · ');
  }

  function renderReplayDetails() {
    elements.replayDetails.replaceChildren();
    const label = documentObject.createElement('span');
    label.className = 'launcher-section-label';
    label.textContent = 'WYBRANA POWTÓRKA';
    const name = documentObject.createElement('strong');
    const details = documentObject.createElement('p');
    if (selectedReplay) {
      name.textContent = String(selectedReplay.name ?? selectedReplay.id ?? 'Powtórka');
      details.textContent = replayMeta(selectedReplay) || 'Zapis meczu serwera.';
      elements.replaySummary.textContent = name.textContent;
    } else {
      name.textContent = 'Nie wybrano meczu';
      details.textContent = 'Wybierz pozycję z listy.';
      elements.replaySummary.textContent = 'Wybierz powtórkę';
    }
    elements.replayDetails.append(label, name, details);
    elements.replayPlay.disabled = busy || !selectedReplay;
  }

  function renderReplays() {
    const query = elements.replaySearch.value.trim().toLocaleLowerCase('pl-PL');
    const filtered = replays.filter((replay) => {
      if (!query) return true;
      return [replay?.name, replay?.id, replayMap(replay), replay?.metadata?.gameType]
        .filter(Boolean)
        .some((value) => String(value).toLocaleLowerCase('pl-PL').includes(query));
    });

    elements.replayList.replaceChildren();
    elements.replayCount.textContent = String(filtered.length);
    if (filtered.length === 0) {
      const empty = documentObject.createElement('p');
      empty.className = 'empty-state';
      empty.textContent = replays.length ? 'Brak wyników wyszukiwania.' : 'Brak zapisanych powtórek.';
      elements.replayList.append(empty);
    } else {
      for (const replay of filtered) {
        const id = replayId(replay);
        const button = documentObject.createElement('button');
        button.type = 'button';
        button.className = 'launcher-replay-item';
        button.role = 'option';
        button.dataset.replayId = id;
        button.setAttribute('aria-selected', String(id && id === replayId(selectedReplay)));
        const copy = documentObject.createElement('span');
        const name = documentObject.createElement('strong');
        name.textContent = String(replay?.name ?? replay?.id ?? 'Powtórka');
        const meta = documentObject.createElement('small');
        meta.textContent = replayMeta(replay);
        const duration = documentObject.createElement('span');
        duration.textContent = durationLabel(replay);
        copy.append(name, meta);
        button.append(copy, duration);
        elements.replayList.append(button);
      }
    }
    renderReplayDetails();
  }

  function renderStatus() {
    const online = Boolean(status?.online ?? status?.dedicated?.running ?? status?.running);
    const players = Math.max(0, numberOr(status?.players ?? status?.udp?.clients, 0));
    const message = String(status?.message ?? (status === null
      ? 'Sprawdzanie serwera…'
      : (online ? 'Serwer działa' : 'Serwer jest niedostępny')));
    elements.serverStatus.dataset.online = String(online);
    elements.serverBrief.dataset.online = String(online);
    elements.serverStatusText.textContent = message;
    elements.serverPlayers.textContent = online ? `${players} gr.` : '—';
    const briefCopy = elements.serverBrief.querySelector(':scope > div > span');
    if (briefCopy) briefCopy.textContent = online ? `${players} graczy online` : message;
    elements.multiplayerPlay.disabled = busy || !online || maps.length === 0;
  }

  function renderNavigation() {
    for (const button of root.querySelectorAll('[data-launcher-view]')) {
      const active = button.dataset.launcherView === state.activeView;
      if (active) button.setAttribute('aria-current', 'page');
      else button.removeAttribute('aria-current');
    }
    for (const panel of root.querySelectorAll('[data-launcher-panel]')) {
      panel.hidden = panel.dataset.launcherPanel !== state.activeView;
    }
    const topbar = {
      campaign: documentObject.getElementById('campaignButton'),
      multiplayer: documentObject.getElementById('multiplayerButton'),
      replays: documentObject.getElementById('replayButton')
    };
    for (const [view, button] of Object.entries(topbar)) {
      button?.setAttribute('aria-pressed', String(view === state.activeView));
    }
  }

  function renderAll() {
    reconcileProfileForMode();
    renderNavigation();
    renderDifficulty();
    renderModes();
    renderMaps();
    renderTeams();
    renderModels();
    renderSettings();
    renderReplays();
    renderStatus();
  }

  function announce(message, level = 'info') {
    const text = String(message ?? '').trim();
    elements.announcement.hidden = !text;
    elements.announcement.dataset.level = level;
    elements.announcement.textContent = text;
  }

  async function invoke(callback, payload, pendingMessage) {
    if (busy || typeof callback !== 'function') return;
    announce('');
    api.setBusy(true, pendingMessage);
    try {
      await callback(payload);
    } catch (error) {
      api.announceError(error?.message ?? error);
    } finally {
      api.setBusy(false);
    }
  }

  function updateSettings(patch) {
    state.settings = normalizedSettings({ ...state.settings, ...patch });
    persist();
    renderSettings();
  }

  root.addEventListener('click', (event) => {
    const viewButton = event.target.closest('[data-launcher-view]');
    if (viewButton) {
      api.select(viewButton.dataset.launcherView);
      return;
    }
    const difficultyButton = event.target.closest('[data-difficulty]');
    if (difficultyButton) {
      const item = difficulties.find((entry) => entry.id === difficultyButton.dataset.difficulty);
      state.difficulty = normalizedDifficulty(item?.value, difficulties);
      persist();
      renderDifficulty();
      return;
    }
    const modeButton = event.target.closest('[data-mode]');
    if (modeButton) {
      const mode = modeButton.dataset.mode;
      const currentMap = maps.find((map) => map.id === selectedMap());
      const nextMap = currentMap && (currentMap.modes.length === 0 || currentMap.modes.includes(mode))
        ? currentMap
        : maps.find((map) => map.modes.length === 0 || map.modes.includes(mode));
      replaceMatch({ mode, map: nextMap?.id ?? selectedMap() });
      reconcileProfileForMode();
      renderModes();
      renderMaps();
      renderTeams();
      renderModels();
      return;
    }
    const mapButton = event.target.closest('[data-map]');
    if (mapButton) {
      replaceMatch({ map: mapButton.dataset.map, mapId: mapButton.dataset.map });
      renderMaps();
      return;
    }
    const teamButton = event.target.closest('[data-team]');
    if (teamButton) {
      const team = teamButton.dataset.team;
      const selectedModel = models.find((model) => model.id === state.profile?.model);
      const model = isTeamMode() && selectedModel?.team && selectedModel.team !== team
        ? 'auto'
        : state.profile?.model;
      state.profile = normalizedProfile({
        ...state.profile,
        team,
        model
      }, models);
      reconcileProfileForMode();
      persist();
      renderTeams();
      renderModels();
      return;
    }
    const replayButton = event.target.closest('[data-replay-id]');
    if (replayButton) {
      selectedReplay = replays.find((replay) => replayId(replay) === replayButton.dataset.replayId) ?? null;
      renderReplays();
    }
  });

  elements.model.addEventListener('change', () => {
    const model = models.find((entry) => entry.id === elements.model.value);
    const team = isTeamMode() && model?.team
      ? model.team
      : state.profile?.team;
    state.profile = normalizedProfile({ ...state.profile, team, model: elements.model.value }, models);
    persist();
    renderTeams();
    renderModels();
  });

  elements.bots.addEventListener('input', () => {
    replaceMatch({ bots: Number(elements.bots.value), botCount: Number(elements.bots.value) });
    renderMatchControls();
  });
  elements.fragLimit.addEventListener('input', () => {
    replaceMatch({ fragLimit: Number(elements.fragLimit.value) });
    renderMatchControls();
  });
  elements.timeLimit.addEventListener('input', () => {
    replaceMatch({ timeLimit: Number(elements.timeLimit.value) });
    renderMatchControls();
  });

  elements.sensitivity.addEventListener('input', () => updateSettings({ sensitivity: Number(elements.sensitivity.value) }));
  elements.invertY.addEventListener('change', () => updateSettings({ invertY: elements.invertY.checked }));
  elements.fov.addEventListener('input', () => updateSettings({ fov: Number(elements.fov.value) }));
  elements.masterVolume.addEventListener('input', () => updateSettings({ masterVolume: Number(elements.masterVolume.value) / 100 }));
  elements.musicVolume.addEventListener('input', () => updateSettings({ musicVolume: Number(elements.musicVolume.value) / 100 }));

  elements.replaySearch.addEventListener('input', renderReplays);
  elements.replayRefresh.addEventListener('click', async () => {
    if (busy || typeof options.onRefreshReplays !== 'function') return;
    elements.replayRefresh.disabled = true;
    try {
      const list = await options.onRefreshReplays();
      if (Array.isArray(list)) api.setReplays(list);
    } catch (error) {
      api.announceError(error?.message ?? error);
    } finally {
      elements.replayRefresh.disabled = false;
    }
  });

  elements.campaignPlay.addEventListener('click', () => invoke(options.onCampaign, {
    difficulty: state.difficulty,
    settings: clone(state.settings)
  }, 'Uruchamianie kampanii'));

  elements.multiplayerPlay.addEventListener('click', () => invoke(options.onMultiplayer, {
    match: clone(normalizedMatch(state.match)),
    profile: clone(normalizedProfile(state.profile, models)),
    settings: clone(state.settings)
  }, 'Przygotowanie meczu'));

  elements.replayPlay.addEventListener('click', () => {
    if (!selectedReplay) return;
    invoke(options.onReplay, {
      replay: clone(selectedReplay),
      settings: clone(state.settings)
    }, 'Uruchamianie powtórki');
  });

  const api = {
    show(view = state.activeView) {
      root.hidden = false;
      root.setAttribute('aria-hidden', 'false');
      body?.classList.add('launcher-ready', 'launcher-visible');
      this.select(view);
      return this;
    },

    hide() {
      root.hidden = true;
      root.setAttribute('aria-hidden', 'true');
      body?.classList.remove('launcher-visible');
      return this;
    },

    select(view) {
      const next = VIEWS.has(view) ? view : 'campaign';
      state.activeView = next;
      persist();
      announce('');
      renderNavigation();
      options.onSelect?.(next);
      return this;
    },

    setBusy(value, message = '') {
      busy = Boolean(value);
      root.setAttribute('aria-busy', String(busy));
      elements.campaignPlay.disabled = busy;
      elements.multiplayerPlay.disabled = busy;
      elements.replayPlay.disabled = busy || !selectedReplay;
      if (busy && message) announce(message, 'info');
      else if (!busy && elements.announcement.dataset.level !== 'error') announce('');
      renderStatus();
      return this;
    },

    setStatus(nextStatus) {
      status = nextStatus && typeof nextStatus === 'object' ? { ...nextStatus } : null;
      renderStatus();
      return this;
    },

    setReplays(nextReplays) {
      replays = Array.isArray(nextReplays) ? nextReplays.filter(Boolean) : [];
      const selectedId = replayId(selectedReplay);
      selectedReplay = replays.find((replay) => replayId(replay) === selectedId) ?? null;
      if (!selectedReplay && replays.length === 1) selectedReplay = replays[0];
      renderReplays();
      return this;
    },

    setMatchCatalog(catalog = {}) {
      if (catalog.modes) modes = normalizeModes(catalog.modes);
      if (catalog.maps) maps = normalizeMaps(catalog.maps);
      if (catalog.teams) teams = normalizeTeams(catalog.teams);
      if (catalog.models) {
        models = normalizeModels(catalog.models);
        const restoredModel = deferredModel && models.some((model) => model.id === deferredModel)
          ? deferredModel
          : state.profile?.model;
        deferredModel = '';
        state.profile = normalizedProfile({ ...state.profile, model: restoredModel }, models);
      }
      if (catalog.match) state.match = normalizedMatch(catalog.match);
      reconcileProfileForMode();
      persist();
      renderModes();
      renderMaps();
      renderTeams();
      renderModels();
      return this;
    },

    getState() {
      return clone({
        activeView: state.activeView,
        difficulty: state.difficulty,
        match: state.match,
        profile: state.profile,
        settings: state.settings,
        selectedReplayId: replayId(selectedReplay),
        busy,
        status
      });
    },

    announceError(error) {
      announce(error?.message ?? error ?? 'Nie udało się wykonać operacji.', 'error');
      return this;
    }
  };

  renderAll();
  return api;
}
