(function () {
  const STORAGE_KEY = "gallery.rankings.v1";
  const STORAGE_TEST_KEY = `${STORAGE_KEY}.probe`;
  const STATE_VERSION = 1;
  const CLOSE_MATCH_COUNT = 6;
  const REMOTE_RANKINGS_URL = "/api/rankings";
  const REMOTE_RANKINGS_VOTE_URL = "/api/rankings/vote";
  const REMOTE_RANKINGS_RESET_URL = "/api/rankings/reset";
  const REMOTE_RANKINGS_UNDO_URL = "/api/rankings/undo";

  function $(id) {
    return document.getElementById(id);
  }

  function escapeHtml(value) {
    return String(value)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  function formatRating(rating) {
    return new Intl.NumberFormat("en-US", {
      maximumFractionDigits: 0,
    }).format(rating);
  }

  function pluralize(count, singular, plural) {
    return count === 1 ? singular : plural;
  }

  function createPairKey(leftId, rightId) {
    return [leftId, rightId].sort().join(":");
  }

  function isValidMealId(id) {
    return typeof id === "string" && /^\d+$/.test(id);
  }

  function createDefaultEntry(id, defaultRating) {
    return {
      id,
      rating: defaultRating,
      wins: 0,
      losses: 0,
    };
  }

  function cloneEntry(entry) {
    return {
      id: entry.id,
      rating: entry.rating,
      wins: entry.wins,
      losses: entry.losses,
    };
  }

  function roundRating(rating) {
    return Math.round(rating * 1000) / 1000;
  }

  function pickRandom(items) {
    return items[Math.floor(Math.random() * items.length)];
  }

  function parseSeedData() {
    const seedElement = $("rankings-seed-data");

    if (!seedElement) {
      throw new Error("Missing rankings seed data");
    }

    const seedData = JSON.parse(seedElement.textContent);

    if (!seedData || !Array.isArray(seedData.meals) || !seedData.elo) {
      throw new Error("Invalid rankings seed data");
    }

    return seedData;
  }

  async function requestJson(url, options) {
    const response = await fetch(url, {
      cache: "no-store",
      headers: {
        Accept: "application/json",
        ...(options && options.body ? { "Content-Type": "application/json" } : {}),
      },
      ...options,
    });

    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }

    return response.json();
  }

  function createLocalPersistence() {
    let available = false;

    try {
      localStorage.setItem(STORAGE_TEST_KEY, "1");
      localStorage.removeItem(STORAGE_TEST_KEY);
      available = true;
    } catch (error) {
      available = false;
    }

    return {
      get available() {
        return available;
      },
      load() {
        if (!available) {
          return null;
        }

        try {
          const raw = localStorage.getItem(STORAGE_KEY);
          return raw ? JSON.parse(raw) : null;
        } catch (error) {
          return null;
        }
      },
      save(state) {
        if (!available) {
          return;
        }

        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
        } catch (error) {
          available = false;
        }
      },
      clear() {
        if (!available) {
          return;
        }

        try {
          localStorage.removeItem(STORAGE_KEY);
        } catch (error) {
          available = false;
        }
      },
    };
  }

  function createPersistence() {
    const local = createLocalPersistence();
    let mode = "memory";
    let pendingNotice = null;

    function setMode(nextMode, nextNotice) {
      if (mode !== nextMode && nextNotice) {
        pendingNotice = nextNotice;
      }

      mode = nextMode;
    }

    function getFallbackMode() {
      return local.available ? "local" : "memory";
    }

    return {
      get mode() {
        return mode;
      },
      async load(seedData) {
        if (typeof fetch === "function") {
          try {
            const remoteState = await requestJson(REMOTE_RANKINGS_URL, { method: "GET" });

            setMode("server");

            return syncStoredState(seedData, remoteState);
          } catch (error) {
            setMode(getFallbackMode());
          }
        } else {
          setMode(getFallbackMode());
        }

        return syncStoredState(seedData, local.load());
      },
      async save(state) {
        if (mode === "local") {
          local.save(state);
        }

        return state;
      },
      async submitVote(seedData, state, winnerId, loserId, pairKey, leftId, rightId) {
        if (mode === "server" && typeof fetch === "function") {
          try {
            const remoteState = await requestJson(REMOTE_RANKINGS_VOTE_URL, {
              method: "POST",
              body: JSON.stringify({
                winnerId,
                loserId,
                pairKey,
                leftId,
                rightId,
              }),
            });

            return syncStoredState(seedData, remoteState);
          } catch (error) {
            setMode(
              getFallbackMode(),
              local.available
                ? "Server sync failed, so votes are now saved only in this browser."
                : "Server sync failed, so votes will reset when you reload."
            );
          }
        }

        const nextState = applyVote(
          seedData,
          state,
          winnerId,
          loserId,
          pairKey,
          leftId,
          rightId
        );

        if (mode === "local") {
          local.save(nextState);
        }

        return nextState;
      },
      async undo(seedData, state) {
        if (mode === "server" && typeof fetch === "function") {
          try {
            const remoteState = await requestJson(REMOTE_RANKINGS_UNDO_URL, {
              method: "POST",
            });

            return syncStoredState(seedData, remoteState);
          } catch (error) {
            setMode(
              getFallbackMode(),
              local.available
                ? "Server sync failed, so go back now applies only in this browser."
                : "Server sync failed, so go back lasts only until you reload."
            );
          }
        }

        const nextState = undoLastVote(seedData, state);

        if (mode === "local") {
          local.save(nextState);
        }

        return nextState;
      },
      async reset(seedData) {
        if (mode === "server" && typeof fetch === "function") {
          try {
            const remoteState = await requestJson(REMOTE_RANKINGS_RESET_URL, {
              method: "POST",
            });

            return syncStoredState(seedData, remoteState);
          } catch (error) {
            setMode(
              getFallbackMode(),
              local.available
                ? "Server sync failed, so resets now apply only in this browser."
                : "Server sync failed, so resets last only until you reload."
            );
          }
        }

        const nextState = createSeedState(seedData);

        if (mode === "local") {
          local.clear();
          local.save(nextState);
        }

        return nextState;
      },
      consumeNotice() {
        const notice = pendingNotice;

        pendingNotice = null;

        return notice;
      },
    };
  }

  function isValidStoredEntry(entry) {
    if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
      return false;
    }

    if (typeof entry.id !== "string" || !/^\d+$/.test(entry.id)) {
      return false;
    }

    if (typeof entry.rating !== "number" || !Number.isFinite(entry.rating) || entry.rating <= 0) {
      return false;
    }

    return ["wins", "losses"].every(
      (field) => Number.isInteger(entry[field]) && entry[field] >= 0
    );
  }

  function createSeedState(seedData) {
    return {
      version: STATE_VERSION,
      voteCount: 0,
      lastPairKey: null,
      updatedAt: null,
      undo: null,
      elo: {
        defaultRating: seedData.elo.defaultRating,
        kFactor: seedData.elo.kFactor,
        entries: seedData.meals.map((meal) => {
          const seedEntry =
            seedData.elo.entries.find((entry) => entry.id === meal.id) ||
            createDefaultEntry(meal.id, seedData.elo.defaultRating);

          return cloneEntry(seedEntry);
        }),
      },
    };
  }

  function syncStateCore(seedData, storedState) {
    if (!storedState || typeof storedState !== "object" || Array.isArray(storedState)) {
      return createSeedState(seedData);
    }

    const storedEntries = Array.isArray(storedState.elo?.entries)
      ? storedState.elo.entries.filter(isValidStoredEntry)
      : [];
    const storedEntryById = new Map(storedEntries.map((entry) => [entry.id, entry]));
    const seedEntryById = new Map(seedData.elo.entries.map((entry) => [entry.id, entry]));
    const entries = seedData.meals.map((meal) => {
      const storedEntry = storedEntryById.get(meal.id);

      if (storedEntry) {
        return cloneEntry(storedEntry);
      }

      const seedEntry =
        seedEntryById.get(meal.id) || createDefaultEntry(meal.id, seedData.elo.defaultRating);

      return cloneEntry(seedEntry);
    });
    const derivedVoteCount = entries.reduce((sum, entry) => sum + entry.wins, 0);

    return {
      version: STATE_VERSION,
      voteCount:
        Number.isInteger(storedState.voteCount) && storedState.voteCount >= derivedVoteCount
          ? storedState.voteCount
          : derivedVoteCount,
      lastPairKey: typeof storedState.lastPairKey === "string" ? storedState.lastPairKey : null,
      updatedAt: typeof storedState.updatedAt === "string" ? storedState.updatedAt : null,
      undo: null,
      elo: {
        defaultRating: seedData.elo.defaultRating,
        kFactor: seedData.elo.kFactor,
        entries,
      },
    };
  }

  function createUndoSnapshot(state) {
    return {
      voteCount: state.voteCount,
      lastPairKey: state.lastPairKey,
      updatedAt: state.updatedAt,
      elo: {
        defaultRating: state.elo.defaultRating,
        kFactor: state.elo.kFactor,
        entries: state.elo.entries.map(cloneEntry),
      },
    };
  }

  function resolveUndoPairIds(winnerId, loserId, leftId, rightId) {
    const validPair =
      isValidMealId(leftId) &&
      isValidMealId(rightId) &&
      leftId !== rightId &&
      [leftId, rightId].includes(winnerId) &&
      [leftId, rightId].includes(loserId);

    if (validPair) {
      return { leftId, rightId };
    }

    return { leftId: winnerId, rightId: loserId };
  }

  function syncUndo(seedData, storedUndo) {
    if (!storedUndo || typeof storedUndo !== "object" || Array.isArray(storedUndo)) {
      return null;
    }

    const { leftId, rightId, winnerId, loserId, snapshot } = storedUndo;

    if (
      !isValidMealId(leftId) ||
      !isValidMealId(rightId) ||
      !isValidMealId(winnerId) ||
      !isValidMealId(loserId) ||
      leftId === rightId ||
      winnerId === loserId
    ) {
      return null;
    }

    const pairIds = new Set([leftId, rightId]);
    const mealIds = new Set(seedData.meals.map((meal) => meal.id));

    if (
      !pairIds.has(winnerId) ||
      !pairIds.has(loserId) ||
      ![leftId, rightId, winnerId, loserId].every((id) => mealIds.has(id))
    ) {
      return null;
    }

    return {
      pairKey: createPairKey(leftId, rightId),
      leftId,
      rightId,
      winnerId,
      loserId,
      snapshot: createUndoSnapshot(syncStateCore(seedData, snapshot)),
    };
  }

  function syncStoredState(seedData, storedState) {
    const nextState = syncStateCore(seedData, storedState);

    return {
      ...nextState,
      undo: syncUndo(seedData, storedState?.undo),
    };
  }

  function restoreUndoSnapshot(seedData, snapshot) {
    return {
      ...syncStateCore(seedData, snapshot),
      undo: null,
    };
  }

  function compareRankedMeals(left, right) {
    const leftMatches = left.wins + left.losses;
    const rightMatches = right.wins + right.losses;

    if (right.rating !== left.rating) {
      return right.rating - left.rating;
    }

    if (rightMatches !== leftMatches) {
      return rightMatches - leftMatches;
    }

    return Number.parseInt(left.id, 10) - Number.parseInt(right.id, 10);
  }

  function getRankedMeals(meals, eloData) {
    const entryById = new Map(eloData.entries.map((entry) => [entry.id, entry]));

    return meals
      .map((meal) => {
        const entry =
          entryById.get(meal.id) || createDefaultEntry(meal.id, eloData.defaultRating);

        return {
          ...meal,
          rating: entry.rating,
          wins: entry.wins,
          losses: entry.losses,
          matches: entry.wins + entry.losses,
        };
      })
      .sort(compareRankedMeals);
  }

  function getRankingMeta(rankedMeal) {
    const ratingText = `Elo ${formatRating(rankedMeal.rating)}`;

    if (rankedMeal.matches === 0) {
      return `${ratingText} | no votes yet`;
    }

    return `${ratingText} | ${rankedMeal.wins}-${rankedMeal.losses} record across ${
      rankedMeal.matches
    } ${pluralize(rankedMeal.matches, "match", "matches")}`;
  }

  function expectedScore(rating, opponentRating) {
    return 1 / (1 + Math.pow(10, (opponentRating - rating) / 400));
  }

  function applyVote(seedData, state, winnerId, loserId, pairKey, leftId, rightId) {
    const entryById = new Map(
      state.elo.entries.map((entry) => [entry.id, cloneEntry(entry)])
    );
    const winner = entryById.get(winnerId);
    const loser = entryById.get(loserId);
    const undoPair = resolveUndoPairIds(winnerId, loserId, leftId, rightId);
    const resolvedPairKey =
      typeof pairKey === "string" && pairKey.length > 0
        ? pairKey
        : createPairKey(undoPair.leftId, undoPair.rightId);

    if (!winner || !loser) {
      return state;
    }

    const winnerExpected = expectedScore(winner.rating, loser.rating);
    const loserExpected = expectedScore(loser.rating, winner.rating);

    winner.rating = roundRating(winner.rating + state.elo.kFactor * (1 - winnerExpected));
    loser.rating = roundRating(loser.rating + state.elo.kFactor * (0 - loserExpected));
    winner.wins += 1;
    loser.losses += 1;

    return {
      ...state,
      voteCount: state.voteCount + 1,
      lastPairKey: resolvedPairKey,
      updatedAt: new Date().toISOString(),
      undo: {
        pairKey: createPairKey(undoPair.leftId, undoPair.rightId),
        leftId: undoPair.leftId,
        rightId: undoPair.rightId,
        winnerId,
        loserId,
        snapshot: createUndoSnapshot(state),
      },
      elo: {
        ...state.elo,
        entries: seedData.meals.map((meal) => entryById.get(meal.id)),
      },
    };
  }

  function undoLastVote(seedData, state) {
    if (!state.undo) {
      return state;
    }

    return restoreUndoSnapshot(seedData, state.undo.snapshot);
  }

  function choosePair(rankedMeals, avoidedPairKeys) {
    if (rankedMeals.length < 2) {
      return null;
    }

    const avoided = new Set(avoidedPairKeys.filter(Boolean));
    const rankedOrder = new Map(rankedMeals.map((meal, index) => [meal.id, index]));

    for (let attempt = 0; attempt < 20; attempt += 1) {
      const baseMeal = rankedMeals[Math.floor(Math.random() * rankedMeals.length)];
      const candidates = rankedMeals
        .filter((meal) => meal.id !== baseMeal.id)
        .sort((left, right) => {
          const ratingGap = Math.abs(left.rating - baseMeal.rating) - Math.abs(right.rating - baseMeal.rating);

          if (ratingGap !== 0) {
            return ratingGap;
          }

          return (
            Math.abs(rankedOrder.get(left.id) - rankedOrder.get(baseMeal.id)) -
            Math.abs(rankedOrder.get(right.id) - rankedOrder.get(baseMeal.id))
          );
        });
      const closeCandidates = candidates.slice(0, Math.min(CLOSE_MATCH_COUNT, candidates.length));
      const filteredCandidates = closeCandidates.filter(
        (meal) => !avoided.has(createPairKey(baseMeal.id, meal.id))
      );
      const candidatePool =
        filteredCandidates.length > 0
          ? filteredCandidates
          : candidates.filter((meal) => !avoided.has(createPairKey(baseMeal.id, meal.id)));
      const fallbackPool = candidatePool.length > 0 ? candidatePool : candidates;
      const opponent = pickRandom(
        fallbackPool.slice(0, Math.min(CLOSE_MATCH_COUNT, fallbackPool.length))
      );

      if (opponent) {
        return Math.random() < 0.5
          ? { left: baseMeal, right: opponent }
          : { left: opponent, right: baseMeal };
      }
    }

    return {
      left: rankedMeals[0],
      right: rankedMeals[1],
    };
  }

  function renderDuelCard(meal, sideLabel, disabled) {
    const disabledAttributes = disabled ? ' disabled aria-disabled="true"' : "";

    return `<article class="duel-card">
      <button class="duel-card__button" type="button" data-meal-id="${meal.id}"${disabledAttributes}>
        <div class="duel-card__media">
          <img src="images/fulls/${meal.id}.jpg" alt="${escapeHtml(`${meal.title} photo`)}" />
        </div>
        <div class="duel-card__body">
          <p class="duel-card__label">${escapeHtml(sideLabel)}</p>
          <h3 class="duel-card__title">${escapeHtml(meal.title)}</h3>
          <p class="duel-card__meta">${escapeHtml(getRankingMeta(meal))}</p>
          <p class="duel-card__description">${escapeHtml(meal.description)}</p>
          <span class="duel-card__cta">Choose this meal</span>
        </div>
      </button>
      <a class="duel-card__open" href="images/fulls/${meal.id}.jpg" target="_blank" rel="noreferrer">open full image</a>
    </article>`;
  }

  function renderRankingCard(meal, placement) {
    return `<article class="ranking-card">
      <p class="ranking-card__placement">#${placement}</p>
      <a class="ranking-card__thumbnail" href="images/fulls/${meal.id}.jpg">
        <img src="images/thumbs/${meal.id}.jpg" alt="${escapeHtml(`${meal.title} thumbnail`)}" loading="lazy" />
      </a>
      <div class="ranking-card__body">
        <h2>${escapeHtml(meal.title)}</h2>
        <p class="ranking-card__meta">${escapeHtml(getRankingMeta(meal))}</p>
        <p>${escapeHtml(meal.description)}</p>
      </div>
    </article>`;
  }

  function getSummaryText(seedData, state, persistence) {
    const voteText = `${state.voteCount} ${pluralize(state.voteCount, "vote", "votes")}`;

    if (persistence.mode === "server") {
      return `${seedData.meals.length} meals ranked. ${voteText} saved on the server.`;
    }

    if (persistence.mode === "local") {
      return `${seedData.meals.length} meals ranked. ${voteText} saved only in this browser.`;
    }

    return `${seedData.meals.length} meals ranked. ${voteText} active for this session only.`;
  }

  function getStatusText(persistence) {
    if (persistence.mode === "server") {
      return "Votes are saved on the server and shared across browsers.";
    }

    if (persistence.mode === "local") {
      return "Server sync is unavailable, so votes are saved only in this browser.";
    }

    return "Browser storage is unavailable, so votes reset when you reload.";
  }

  async function init() {
    let seedData;

    try {
      seedData = parseSeedData();
    } catch (error) {
      console.error(error);
      return;
    }

    const elements = {
      duelCards: $("duel-cards"),
      rankings: $("rankings"),
      rankingSummary: document.querySelector("#rankings-summary .ranking-summary"),
      voteStatus: $("vote-status"),
      voteMessage: $("vote-message"),
      skipPair: $("skip-pair"),
      undoVote: $("undo-vote"),
      resetRankings: $("reset-rankings"),
    };

    if (
      !elements.duelCards ||
      !elements.rankings ||
      !elements.rankingSummary ||
      !elements.voteStatus ||
      !elements.voteMessage ||
      !elements.skipPair ||
      !elements.undoVote ||
      !elements.resetRankings
    ) {
      return;
    }

    elements.voteMessage.textContent = "Loading saved rankings...";

    const persistence = createPersistence();
    const mealById = new Map(seedData.meals.map((meal) => [meal.id, meal]));
    let state = await persistence.load(seedData);
    let currentPair = null;
    let currentPairKey = null;
    let pendingPairIds = null;
    let lastMessage = "Choose the better meal to start ranking.";
    let busy = false;

    await persistence.save(state);

    function queueNextPair(rankedMeals) {
      if (pendingPairIds) {
        const rankedMealById = new Map(rankedMeals.map((meal) => [meal.id, meal]));
        const left = rankedMealById.get(pendingPairIds.leftId);
        const right = rankedMealById.get(pendingPairIds.rightId);

        pendingPairIds = null;

        if (left && right && left.id !== right.id) {
          currentPair = { left, right };
          currentPairKey = createPairKey(left.id, right.id);
          return;
        }
      }

      currentPair = choosePair(rankedMeals, [currentPairKey, state.lastPairKey]);
      currentPairKey = currentPair
        ? createPairKey(currentPair.left.id, currentPair.right.id)
        : null;
    }

    function render(message) {
      const rankedMeals = getRankedMeals(seedData.meals, state.elo);

      if (message) {
        lastMessage = message;
      }

      if (
        !currentPair ||
        !rankedMeals.some((meal) => meal.id === currentPair.left.id) ||
        !rankedMeals.some((meal) => meal.id === currentPair.right.id)
      ) {
        queueNextPair(rankedMeals);
      }

      elements.voteStatus.textContent = getStatusText(persistence);
      elements.voteMessage.textContent = lastMessage;
      elements.rankingSummary.textContent = getSummaryText(seedData, state, persistence);
      elements.undoVote.disabled = busy || !state.undo;
      elements.resetRankings.disabled = busy;
      elements.rankings.innerHTML = rankedMeals
        .map((meal, index) => renderRankingCard(meal, index + 1))
        .join("");

      if (!currentPair) {
        elements.duelCards.innerHTML =
          '<p class="duel-placeholder">Add at least two meals before starting head-to-head voting.</p>';
        elements.skipPair.disabled = true;
        return;
      }

      elements.skipPair.disabled = busy;
      elements.duelCards.innerHTML = [
        renderDuelCard(currentPair.left, "Left Pick", busy),
        renderDuelCard(currentPair.right, "Right Pick", busy),
      ].join("");
    }

    async function handleVote(winnerId) {
      if (!currentPair || busy) {
        return;
      }

      const loserId = currentPair.left.id === winnerId ? currentPair.right.id : currentPair.left.id;
      const winnerTitle =
        currentPair.left.id === winnerId ? currentPair.left.title : currentPair.right.title;
      const loserTitle =
        currentPair.left.id === winnerId ? currentPair.right.title : currentPair.left.title;
      let nextMessage = `Picked ${winnerTitle} over ${loserTitle}.`;

      busy = true;
      render(`Saving ${winnerTitle} over ${loserTitle}...`);

      try {
        state = await persistence.submitVote(
          seedData,
          state,
          winnerId,
          loserId,
          currentPairKey,
          currentPair.left.id,
          currentPair.right.id
        );
        currentPair = null;

        const notice = persistence.consumeNotice();

        if (notice) {
          nextMessage = `${nextMessage} ${notice}`;
        }
      } catch (error) {
        console.error(error);
        nextMessage = "Failed to save that vote.";
      } finally {
        busy = false;
        render(nextMessage);
      }
    }

    async function handleUndo() {
      if (!state.undo || busy) {
        return;
      }

      const undoInfo = state.undo;
      const winnerTitle = mealById.get(undoInfo.winnerId)?.title || "that meal";
      const loserTitle = mealById.get(undoInfo.loserId)?.title || "the other meal";
      let nextMessage = `Went back before ${winnerTitle} over ${loserTitle}. Pick again.`;

      busy = true;
      render(`Going back before ${winnerTitle} over ${loserTitle}...`);

      try {
        state = await persistence.undo(seedData, state);
        currentPair = null;
        currentPairKey = null;
        pendingPairIds = {
          leftId: undoInfo.leftId,
          rightId: undoInfo.rightId,
        };

        const notice = persistence.consumeNotice();

        if (notice) {
          nextMessage = `${nextMessage} ${notice}`;
        }
      } catch (error) {
        console.error(error);
        nextMessage = "Failed to go back to the previous vote.";
      } finally {
        busy = false;
        render(nextMessage);
      }
    }

    elements.duelCards.addEventListener("click", async (event) => {
      const button = event.target.closest("[data-meal-id]");

      if (!button) {
        return;
      }

      await handleVote(button.getAttribute("data-meal-id"));
    });

    elements.skipPair.addEventListener("click", () => {
      if (busy) {
        return;
      }

      const rankedMeals = getRankedMeals(seedData.meals, state.elo);

      queueNextPair(rankedMeals);
      render("Skipped that pair.");
    });

    elements.undoVote.addEventListener("click", async () => {
      await handleUndo();
    });

    elements.resetRankings.addEventListener("click", async () => {
      if (busy) {
        return;
      }

      if (!window.confirm("Reset the saved rankings back to the seeded board?")) {
        return;
      }

      let nextMessage = "Saved rankings cleared. Back to the seeded board.";

      busy = true;
      render("Resetting saved rankings...");

      try {
        state = await persistence.reset(seedData);
        currentPair = null;
        currentPairKey = null;

        const notice = persistence.consumeNotice();

        if (notice) {
          nextMessage = `${nextMessage} ${notice}`;
        }
      } catch (error) {
        console.error(error);
        nextMessage = "Failed to reset rankings.";
      } finally {
        busy = false;
        render(nextMessage);
      }
    });

    document.addEventListener("keydown", async (event) => {
      const target = event.target;

      if (
        target &&
        ["INPUT", "TEXTAREA", "SELECT", "BUTTON"].includes(target.tagName)
      ) {
        return;
      }

      if (busy) {
        return;
      }

      if (event.key.toLowerCase() === "z" && state.undo) {
        event.preventDefault();
        await handleUndo();
        return;
      }

      if (!currentPair) {
        return;
      }

      if (event.key === "ArrowLeft") {
        event.preventDefault();
        await handleVote(currentPair.left.id);
      } else if (event.key === "ArrowRight") {
        event.preventDefault();
        await handleVote(currentPair.right.id);
      }
    });

    render();
  }

  init().catch((error) => {
    console.error(error);
  });
})();
