const PAGE = document.body.dataset.page;
const SETUP_PAGE = "application.html";

const STORAGE_KEY = "chesscubing-arena-state-v2";
const WINDOW_NAME_KEY = "chesscubing-arena-state-v2:";
const ASSET_TOKEN_STORAGE_KEY = "chesscubing-arena-asset-token";
const DEFAULT_BLOCK_DURATION_MS = 180000;
const DEFAULT_MOVE_LIMIT_MS = 20000;
const TIME_MODE_INITIAL_CLOCK_MS = 600000;
const CUBE_TIME_CAP_MS = 120000;
const CUBE_START_HOLD_MS = 2000;
const CUBE_PHASE_ALERT_NAVIGATION_DELAY_MS = 700;

const PRESETS = {
  fast: {
    label: "FAST",
    quota: 6,
    description: "6 coups par joueur.",
  },
  freeze: {
    label: "FREEZE",
    quota: 8,
    description: "8 coups par joueur.",
  },
  masters: {
    label: "MASTERS",
    quota: 10,
    description: "10 coups par joueur.",
  },
};

const MODES = {
  twice: {
    label: "ChessCubing Twice",
    subtitle: "Le gagnant du cube ouvre le Block suivant.",
  },
  time: {
    label: "ChessCubing Time",
    subtitle: "Chronos cumules et alternance bloc - / bloc +.",
  },
};

syncViewportHeight();
window.addEventListener("load", syncViewportHeight);
window.addEventListener("resize", syncViewportHeight);
window.addEventListener("scroll", syncViewportHeight, { passive: true });
window.addEventListener("pageshow", syncViewportHeight);
window.addEventListener("orientationchange", syncViewportHeight);
window.visualViewport?.addEventListener("resize", syncViewportHeight);
window.visualViewport?.addEventListener("scroll", syncViewportHeight);
window.setTimeout(syncViewportHeight, 0);
window.setTimeout(syncViewportHeight, 150);
window.setTimeout(syncViewportHeight, 400);
window.requestAnimationFrame(() => {
  window.requestAnimationFrame(syncViewportHeight);
});

let match = readStoredMatch();
let dirty = false;
let audioContext = null;
let cubePhaseAlertRetryCleanup = null;

if (match) {
  if (normalizeRecoveredMatch(match)) {
    dirty = true;
  }
  if (syncRunningState(match)) {
    dirty = true;
  }
}

window.addEventListener("beforeunload", flushState);
document.addEventListener("pointerdown", tryUnlockAudioContext, { passive: true });
document.addEventListener("keydown", tryUnlockAudioContext);
document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    if (match) {
      syncRunningState(match);
    }
    flushState();
  }
});

window.setInterval(() => {
  if (dirty) {
    persistMatch();
  }
}, 1000);

switch (PAGE) {
  case "setup":
    initSetupPage();
    break;
  case "chrono":
    initChronoPage();
    break;
  case "cube":
    initCubePage();
    break;
  default:
    break;
}

function syncViewportHeight() {
  const visibleHeight = window.visualViewport?.height ?? window.innerHeight;
  const viewportTopOffset = window.visualViewport?.offsetTop ?? 0;
  const viewportHeight = Math.max(
    visibleHeight + viewportTopOffset,
    window.innerHeight,
    document.documentElement.clientHeight,
  );

  document.documentElement.style.setProperty("--app-visible-height", `${Math.round(visibleHeight)}px`);
  document.documentElement.style.setProperty("--app-viewport-top", `${Math.round(viewportTopOffset)}px`);
  document.documentElement.style.setProperty("--app-viewport-height", `${Math.round(viewportHeight)}px`);
}

function getAudioContext() {
  const AudioContextClass = window.AudioContext || window.webkitAudioContext;
  if (!AudioContextClass) {
    return null;
  }

  if (!audioContext) {
    try {
      audioContext = new AudioContextClass();
    } catch {
      return null;
    }
  }

  return audioContext;
}

function tryUnlockAudioContext() {
  const context = getAudioContext();
  if (!context || context.state === "running") {
    return;
  }

  context.resume().catch(() => undefined);
}

function playCubePhaseAlert() {
  const context = getAudioContext();
  if (!context || context.state !== "running") {
    return false;
  }

  const pattern = [
    { frequency: 740, offset: 0, duration: 0.12, gain: 0.035 },
    { frequency: 988, offset: 0.18, duration: 0.12, gain: 0.04 },
    { frequency: 1318, offset: 0.36, duration: 0.2, gain: 0.05 },
  ];
  const startAt = context.currentTime + 0.02;

  pattern.forEach(({ frequency, offset, duration, gain }) => {
    const oscillator = context.createOscillator();
    const envelope = context.createGain();
    const toneStartAt = startAt + offset;

    oscillator.type = "triangle";
    oscillator.frequency.setValueAtTime(frequency, toneStartAt);
    envelope.gain.setValueAtTime(0.0001, toneStartAt);
    envelope.gain.exponentialRampToValueAtTime(gain, toneStartAt + 0.02);
    envelope.gain.exponentialRampToValueAtTime(0.0001, toneStartAt + duration);

    oscillator.connect(envelope);
    envelope.connect(context.destination);
    oscillator.start(toneStartAt);
    oscillator.stop(toneStartAt + duration + 0.03);
  });

  return true;
}

function clearCubePhaseAlertRetry() {
  if (typeof cubePhaseAlertRetryCleanup === "function") {
    cubePhaseAlertRetryCleanup();
    cubePhaseAlertRetryCleanup = null;
  }
}

function scheduleCubePhaseAlertRetry(storedMatch) {
  if (!storedMatch?.cube?.phaseAlertPending || cubePhaseAlertRetryCleanup) {
    return;
  }

  const tryPlayOnGesture = () => {
    tryUnlockAudioContext();
    if (!playCubePhaseAlert()) {
      return;
    }

    storedMatch.cube.phaseAlertPending = false;
    dirty = true;
    persistMatch();
    clearCubePhaseAlertRetry();
  };

  const events = ["pointerdown", "keydown"];
  cubePhaseAlertRetryCleanup = () => {
    events.forEach((eventName) => {
      document.removeEventListener(eventName, tryPlayOnGesture);
    });
  };

  events.forEach((eventName) => {
    document.addEventListener(eventName, tryPlayOnGesture, { passive: eventName === "pointerdown" });
  });
}

function maybePlayPendingCubePhaseAlert(storedMatch) {
  if (!storedMatch?.cube?.phaseAlertPending) {
    clearCubePhaseAlertRetry();
    return false;
  }

  tryUnlockAudioContext();
  if (!playCubePhaseAlert()) {
    scheduleCubePhaseAlertRetry(storedMatch);
    return false;
  }

  storedMatch.cube.phaseAlertPending = false;
  dirty = true;
  persistMatch();
  clearCubePhaseAlertRetry();
  return true;
}

function initSetupPage() {
  const form = document.querySelector("#setupForm");
  const summary = document.querySelector("#setupSummary");
  const loadDemoButton = document.querySelector("#loadDemoButton");
  const resumeCard = document.querySelector("#resumeCard");
  const refreshAppButton = document.querySelector("#refreshAppButton");
  const optionCards = Array.from(document.querySelectorAll(".option-card"));
  const competitionModeInput = document.querySelector("#competitionMode");
  const competitionFields = Array.from(document.querySelectorAll("[data-competition-field]"));
  const moveSecondsField = document.querySelector("#moveSecondsField");
  const timeInitialField = document.querySelector("#timeInitialField");
  const blockDurationLabel = document.querySelector("#blockDurationLabel");
  const moveSecondsInput = form?.querySelector('[name="moveSeconds"]');
  const timeInitialInput = form?.querySelector('[name="timeInitialMinutes"]');

  if (!form || !summary || !loadDemoButton || !resumeCard) {
    return;
  }

  const syncCompetitionFields = () => {
    const competitionMode = competitionModeInput instanceof HTMLInputElement && competitionModeInput.checked;

    competitionFields.forEach((field) => {
      if (!(field instanceof HTMLElement)) {
        return;
      }

      field.hidden = !competitionMode;
      field
        .querySelectorAll("input, textarea, select")
        .forEach((input) => {
          if (
            input instanceof HTMLInputElement ||
            input instanceof HTMLTextAreaElement ||
            input instanceof HTMLSelectElement
          ) {
            input.disabled = !competitionMode;
          }
        });
    });
  };

  const syncOptionCardStates = () => {
    optionCards.forEach((card) => {
      if (!(card instanceof HTMLElement)) {
        return;
      }

      const input = card.querySelector('input[type="radio"], input[type="checkbox"]');
      if (!(input instanceof HTMLInputElement)) {
        return;
      }

      card.classList.toggle("is-selected", input.checked);
    });
  };

  const renderSummary = () => {
    const mode = getRadioValue(form, "mode") || "twice";
    const preset = getRadioValue(form, "preset") || "fast";
    const quota = PRESETS[preset].quota;
    const blockDurationMs = getMinuteInputMs(form, "blockMinutes", DEFAULT_BLOCK_DURATION_MS);
    const moveLimitMs = getDurationInputMs(form, "moveSeconds", DEFAULT_MOVE_LIMIT_MS);
    const timeInitialMs = getMinuteInputMs(form, "timeInitialMinutes", TIME_MODE_INITIAL_CLOCK_MS);
    const blockLabel = getBlockLabel(mode);
    const moveLimitActive = usesMoveLimit(mode);
    const timeImpact =
      mode === "time"
        ? `Chronos cumules de ${formatClock(timeInitialMs)} par joueur, ajustes apres chaque phase cube avec plafond de 120 s pris en compte. Aucun temps par coup en mode Time.`
        : "Le gagnant du cube commence le Block suivant, avec double coup V2 possible.";
    const timingText = moveLimitActive
      ? `Temps configures : Block ${formatClock(blockDurationMs)}, coup ${formatClock(moveLimitMs)}.`
      : `Temps configures : Block ${formatClock(blockDurationMs)}, temps de chaque joueur ${formatClock(timeInitialMs)}.`;
    const quotaText = moveLimitActive
      ? `Quota actif : ${quota} coups par joueur.`
      : `Quota actif : ${quota} coups par joueur et par Block.`;

    if (moveSecondsField instanceof HTMLElement) {
      moveSecondsField.hidden = !moveLimitActive;
    }

    if (timeInitialField instanceof HTMLElement) {
      timeInitialField.hidden = moveLimitActive;
    }

    if (moveSecondsInput instanceof HTMLInputElement) {
      moveSecondsInput.disabled = !moveLimitActive;
    }

    if (timeInitialInput instanceof HTMLInputElement) {
      timeInitialInput.disabled = moveLimitActive;
    }

    document.body.classList.toggle("time-setup-mode", !moveLimitActive);

    if (blockDurationLabel instanceof HTMLElement) {
      blockDurationLabel.textContent = "Temps du Block (minutes)";
    }

    syncCompetitionFields();
    syncOptionCardStates();

    summary.innerHTML = `
      <strong>${MODES[mode].label}</strong>
      <span>${PRESETS[preset].description}</span>
      <span>${timingText}</span>
      <span>${timeImpact}</span>
      <span>${quotaText}</span>
    `;
  };

  const renderResume = () => {
    if (!match) {
      resumeCard.classList.add("empty");
      resumeCard.innerHTML = "<p>Aucun match en cours pour l'instant.</p>";
      return;
    }

    resumeCard.classList.remove("empty");
    const phaseLabel = match.result
      ? resultText(match)
      : match.phase === "cube"
        ? "Page cube prete"
        : "Page chrono prete";

    resumeCard.innerHTML = `
      <strong>${escapeHtml(match.config.matchLabel)}</strong>
      <p>${escapeHtml(MODES[match.config.mode].label)}</p>
      <p>${escapeHtml(match.config.whiteName)} vs ${escapeHtml(match.config.blackName)}</p>
      <p>${escapeHtml(phaseLabel)}</p>
      <div class="resume-actions">
        <button class="button primary" id="resumeMatchButton" type="button">
          ${match.result ? "Voir le match" : "Reprendre la phase"}
        </button>
        <button class="button ghost" id="clearMatchButton" type="button">
          Effacer le match
        </button>
      </div>
    `;

    resumeCard.querySelector("#resumeMatchButton")?.addEventListener("click", () => {
      navigateTo(routeForMatch(match));
    });

    resumeCard.querySelector("#clearMatchButton")?.addEventListener("click", () => {
      clearMatch();
      renderResume();
    });
  };

  form.addEventListener("input", renderSummary);
  loadDemoButton.addEventListener("click", () => loadDemo(form, renderSummary));
  optionCards.forEach((card) => {
    if (!(card instanceof HTMLElement)) {
      return;
    }

    const input = card.querySelector('input[type="radio"], input[type="checkbox"]');
    if (!(input instanceof HTMLInputElement)) {
      return;
    }

    input.addEventListener("focus", () => {
      card.classList.add("is-focused");
    });

    input.addEventListener("blur", () => {
      card.classList.remove("is-focused");
    });
  });
  refreshAppButton?.addEventListener("click", async () => {
    if (!(refreshAppButton instanceof HTMLButtonElement)) {
      return;
    }

    refreshAppButton.disabled = true;
    refreshAppButton.textContent = "Mise a jour...";
    await forceRefreshToLatest(SETUP_PAGE);
  });

  form.addEventListener("submit", (event) => {
    event.preventDefault();
    const data = new FormData(form);
    const config = {
      matchLabel: sanitizeText(data.get("matchLabel")) || "Rencontre ChessCubing",
      competitionMode: isCheckboxChecked(form, "competitionMode"),
      mode: getRadioValue(form, "mode") || "twice",
      preset: getRadioValue(form, "preset") || "fast",
      blockDurationMs: getMinuteInputMs(form, "blockMinutes", DEFAULT_BLOCK_DURATION_MS),
      moveLimitMs: getDurationInputMs(form, "moveSeconds", DEFAULT_MOVE_LIMIT_MS),
      timeInitialMs: getMinuteInputMs(form, "timeInitialMinutes", TIME_MODE_INITIAL_CLOCK_MS),
      whiteName: sanitizeText(data.get("whiteName")) || "Blanc",
      blackName: sanitizeText(data.get("blackName")) || "Noir",
      arbiterName: sanitizeText(data.get("arbiterName")),
      eventName: sanitizeText(data.get("eventName")),
      notes: sanitizeText(data.get("notes")),
    };

    match = createMatch(config);
    dirty = true;
    persistMatch();
    navigateTo("chrono.html");
  });

  renderSummary();
  renderResume();
}

function initChronoPage() {
  if (!match) {
    replaceTo(SETUP_PAGE);
    return;
  }

  let cubeNavigationTimeoutId = null;
  const goToCubePage = () => {
    if (cubeNavigationTimeoutId !== null) {
      return;
    }

    if (match?.cube?.phaseAlertPending) {
      tryUnlockAudioContext();
      if (playCubePhaseAlert()) {
        match.cube.phaseAlertPending = false;
        dirty = true;
        persistMatch();
        cubeNavigationTimeoutId = window.setTimeout(() => {
          replaceTo("cube.html");
        }, CUBE_PHASE_ALERT_NAVIGATION_DELAY_MS);
        return;
      }
    }

    dirty = true;
    persistMatch();
    replaceTo("cube.html");
  };

  if (!match.result && match.phase === "cube") {
    goToCubePage();
    return;
  }

  const refs = {
    title: document.querySelector("#chronoTitle"),
    subtitle: document.querySelector("#chronoSubtitle"),
    blockTimer: document.querySelector("#blockTimer"),
    blockTimerLabel: document.querySelector("#blockTimerLabel"),
    moveTimer: document.querySelector("#moveTimer"),
    moveTimerCard: document.querySelector("#moveTimerCard"),
    centerLabel: document.querySelector("#chronoCenterLabel"),
    centerValue: document.querySelector("#chronoCenterValue"),
    spineLabel: document.querySelector("#spineLabel"),
    spineHeadline: document.querySelector("#spineHeadline"),
    spineText: document.querySelector("#spineText"),
    primaryButton: document.querySelector("#primaryChronoButton"),
    whiteName: document.querySelector("#whiteNameChrono"),
    blackName: document.querySelector("#blackNameChrono"),
    whiteMoves: document.querySelector("#whiteMovesChrono"),
    blackMoves: document.querySelector("#blackMovesChrono"),
    whiteClock: document.querySelector("#whiteClockChrono"),
    blackClock: document.querySelector("#blackClockChrono"),
    whiteHint: document.querySelector("#whiteHintChrono"),
    blackHint: document.querySelector("#blackHintChrono"),
    whiteButton: document.querySelector("#whiteMoveButton"),
    blackButton: document.querySelector("#blackMoveButton"),
    whiteZone: document.querySelector("#whiteZone"),
    blackZone: document.querySelector("#blackZone"),
    openArbiterButton: document.querySelector("#openArbiterButton"),
    closeArbiterButton: document.querySelector("#closeArbiterButton"),
    arbiterModal: document.querySelector("#arbiterModal"),
    arbiterStatus: document.querySelector("#arbiterStatus"),
    arbiterPauseButton: document.querySelector("#arbiterPauseButton"),
    arbiterCloseBlockButton: document.querySelector("#arbiterCloseBlockButton"),
    arbiterTimeoutButton: document.querySelector("#arbiterTimeoutButton"),
    arbiterSwitchTurnButton: document.querySelector("#arbiterSwitchTurnButton"),
    arbiterWhiteWinButton: document.querySelector("#arbiterWhiteWinButton"),
    arbiterBlackWinButton: document.querySelector("#arbiterBlackWinButton"),
    arbiterStopButton: document.querySelector("#arbiterStopButton"),
    arbiterResetButton: document.querySelector("#arbiterResetButton"),
  };

  const openModal = () => toggleModal(refs.arbiterModal, true);
  const closeModal = () => toggleModal(refs.arbiterModal, false);

  refs.openArbiterButton?.addEventListener("click", openModal);
  refs.closeArbiterButton?.addEventListener("click", closeModal);
  refs.arbiterModal?.addEventListener("click", (event) => {
    const target = event.target;
    if (target instanceof HTMLElement && target.dataset.closeModal === "true") {
      closeModal();
    }
  });

  refs.whiteButton?.addEventListener("click", () => handleChronoTap("white"));
  refs.blackButton?.addEventListener("click", () => handleChronoTap("black"));
  refs.primaryButton?.addEventListener("click", handlePrimaryChronoAction);

  refs.arbiterPauseButton?.addEventListener("click", () => {
    syncRunningState(match);
    if (match.result || match.phase !== "block") {
      return;
    }

    if (match.running) {
      pauseBlock(match);
    } else {
      startBlock(match);
    }

    dirty = true;
    render();
  });

  refs.arbiterCloseBlockButton?.addEventListener("click", () => {
    syncRunningState(match);
    if (match.result || match.phase !== "block") {
      return;
    }

    requestBlockClosure(match, `Cloture manuelle ${getBlockGenitivePhrase(match)} demandee par l'arbitre.`);
    dirty = true;
    if (!match.result && match.phase === "cube") {
      goToCubePage();
      return;
    }
    render();
  });

  refs.arbiterTimeoutButton?.addEventListener("click", () => {
    syncRunningState(match);
    if (match.result || match.phase !== "block" || !usesMoveLimit(match)) {
      return;
    }

    registerMoveTimeout(match, false);
    dirty = true;
    render();
  });

  refs.arbiterSwitchTurnButton?.addEventListener("click", () => {
    syncRunningState(match);
    if (match.result || match.phase !== "block") {
      return;
    }

    match.currentTurn = opponentOf(match.currentTurn);
    if (usesMoveLimit(match)) {
      match.moveRemainingMs = getMoveLimitMs(match);
    }
    logEvent(match, "Trait corrige manuellement par l'arbitre.");
    dirty = true;
    render();
  });

  refs.arbiterWhiteWinButton?.addEventListener("click", () => {
    syncRunningState(match);
    setResult(match, "white");
    dirty = true;
    render();
  });

  refs.arbiterBlackWinButton?.addEventListener("click", () => {
    syncRunningState(match);
    setResult(match, "black");
    dirty = true;
    render();
  });

  refs.arbiterStopButton?.addEventListener("click", () => {
    syncRunningState(match);
    setResult(match, "stopped");
    dirty = true;
    render();
  });

  refs.arbiterResetButton?.addEventListener("click", () => {
    clearMatch();
    replaceTo(SETUP_PAGE);
  });

  function handleChronoTap(color) {
    if (!match || match.result || match.phase !== "block") {
      return;
    }

    syncRunningState(match);

    if (!match.running || match.currentTurn !== color) {
      return;
    }

    if (match.doubleCoup.step === 1) {
      registerFreeDoubleMove(match);
    } else {
      registerCountedMove(match, {
        source: match.doubleCoup.step === 2 ? "double" : "standard",
      });
    }

    dirty = true;
    if (!match.result && match.phase === "cube") {
      goToCubePage();
      return;
    }
    render();
  }

  function handlePrimaryChronoAction() {
    syncRunningState(match);

    if (match.result) {
      navigateTo(SETUP_PAGE);
      return;
    }

    if (match.phase !== "block") {
      goToCubePage();
      return;
    }

    if (match.running) {
      pauseBlock(match);
    } else {
      startBlock(match);
    }

    dirty = true;
    render();
  }

  function renderZone(color) {
    const isWhite = color === "white";
    const button = isWhite ? refs.whiteButton : refs.blackButton;
    const name = isWhite ? refs.whiteName : refs.blackName;
    const moves = isWhite ? refs.whiteMoves : refs.blackMoves;
    const clock = isWhite ? refs.whiteClock : refs.blackClock;
    const hint = isWhite ? refs.whiteHint : refs.blackHint;
    const zone = isWhite ? refs.whiteZone : refs.blackZone;
    const active = match.currentTurn === color;
    const unitLabel = getBlockLabel(match);
    const unitPhrase = getBlockPhrase(match);

    name.textContent = playerName(match, color);
    moves.textContent = `${match.moves[color]} / ${match.quota}`;
    clock.textContent = match.clocks ? formatSignedClock(match.clocks[color]) : `Dernier cube ${renderLastCube(match, color)}`;

    button.classList.toggle("active-turn", active && !match.result);
    zone.classList.toggle("active-zone", active && !match.result);
    zone.classList.toggle("has-player-clock", Boolean(match.clocks));
    clock.classList.toggle("player-clock", Boolean(match.clocks));
    clock.classList.toggle("negative-clock", Boolean(match.clocks) && match.clocks[color] < 0);
    clock.classList.toggle("active-clock", Boolean(match.clocks) && active && !match.result);

    if (match.result) {
      button.textContent = resultText(match);
      button.disabled = true;
      hint.textContent = "Le match est termine.";
      return;
    }

    if (!match.running) {
      button.textContent = `${unitLabel} en pause`;
      button.disabled = true;
      hint.textContent = active
        ? `${unitPhrase} n'a pas encore demarre ou a ete mis en pause.`
        : `${playerName(match, match.currentTurn)} reprendra au demarrage.`;
      return;
    }

    if (!active) {
      button.textContent = "Attends";
      button.disabled = true;
      hint.textContent = `${playerName(match, match.currentTurn)} est en train de jouer.`;
      return;
    }

    if (match.doubleCoup.step === 1) {
      button.textContent = "1er coup gratuit";
      button.disabled = false;
      hint.textContent = "Ce coup ne compte pas et ne doit pas donner echec.";
      return;
    }

    if (match.doubleCoup.step === 2) {
      button.textContent = "2e coup du double";
      button.disabled = false;
      hint.textContent = "Ce coup compte dans le quota et l'echec redevient autorise.";
      return;
    }

    button.textContent = "J'ai fini mon coup";
    button.disabled = false;
    hint.textContent = "Tape des que ton coup est joue sur l'echiquier.";
  }

  function render() {
    if (!match) {
      return;
    }

    if (!match.result && match.phase === "cube") {
      goToCubePage();
      return;
    }

    const timeMode = isTimeMode(match);
    const blockHeading = formatBlockHeading(match, match.blockNumber);

    document.body.classList.toggle("time-mode", timeMode);
    refs.title.textContent = match.config.matchLabel;
    refs.subtitle.textContent = `${blockHeading} - ${MODES[match.config.mode].label} - ${renderModeContext(match)}`;
    refs.blockTimerLabel.textContent = "Temps Block";
    refs.blockTimer.textContent = formatClock(match.blockRemainingMs);
    refs.moveTimer.textContent = usesMoveLimit(match) ? formatClock(match.moveRemainingMs) : "--:--";
    refs.moveTimerCard.hidden = timeMode;
    refs.arbiterTimeoutButton.hidden = timeMode;
    refs.arbiterTimeoutButton.disabled = timeMode;
    if (usesMoveLimit(match)) {
      refs.arbiterTimeoutButton.textContent = `Depassement ${formatClock(getMoveLimitMs(match))}`;
    }

    if (match.result) {
      refs.centerLabel.textContent = "Resultat";
      refs.centerValue.textContent = resultText(match);
      refs.spineLabel.textContent = "Termine";
      refs.spineHeadline.textContent = resultText(match);
      refs.spineText.textContent = "Retournez a la configuration pour lancer une nouvelle rencontre.";
      refs.primaryButton.textContent = "Retour a l'accueil";
      refs.arbiterStatus.textContent = "Le match est termine. Vous pouvez revenir a l'accueil ou reinitialiser.";
    } else if (match.running) {
      refs.centerLabel.textContent = "Trait";
      refs.centerValue.textContent = playerName(match, match.currentTurn);
      refs.spineLabel.textContent = "Chrono en cours";
      refs.spineHeadline.textContent = `${blockHeading} actif`;
      refs.spineText.textContent =
        "Chaque joueur tape sur sa grande zone quand son coup est termine. La page cube s'ouvrira automatiquement a la fin de la phase chess.";
      refs.primaryButton.textContent = "Pause arbitre";
      refs.arbiterStatus.textContent = `${blockHeading} en cours. Joueur au trait : ${playerName(match, match.currentTurn)}.`;
    } else {
      refs.centerLabel.textContent = "Trait";
      refs.centerValue.textContent = playerName(match, match.currentTurn);
      refs.spineLabel.textContent = timeMode ? "Etat du Block" : "Pret";
      refs.spineHeadline.textContent = blockHeading;
      refs.spineText.textContent =
        "Demarrez le Block, puis laissez uniquement les deux grandes zones aux joueurs. La page cube prendra automatiquement le relais.";
      refs.primaryButton.textContent = "Demarrer le Block";
      refs.arbiterStatus.textContent = `${blockHeading} pret. ${playerName(match, match.currentTurn)} commencera.`;
    }

    refs.arbiterCloseBlockButton.textContent = "Passer au cube";

    renderZone("black");
    renderZone("white");
  }

  render();

  window.setInterval(() => {
    if (!match) {
      return;
    }

    const changed = syncRunningState(match);
    if (changed) {
      dirty = true;
    }

    if (!match.result && match.phase === "cube") {
      goToCubePage();
      return;
    }

    render();
  }, 100);
}

function initCubePage() {
  if (!match) {
    replaceTo(SETUP_PAGE);
    return;
  }

  if (!match.result && match.phase !== "cube") {
    replaceTo("chrono.html");
    return;
  }

  maybePlayPendingCubePhaseAlert(match);

  const refs = {
    title: document.querySelector("#cubeTitle"),
    subtitle: document.querySelector("#cubeSubtitle"),
    blockLabelText: document.querySelector("#cubeBlockLabelText"),
    blockLabel: document.querySelector("#cubeBlockLabel"),
    elapsed: document.querySelector("#cubeElapsed"),
    centerLabel: document.querySelector("#cubeCenterLabel"),
    centerValue: document.querySelector("#cubeCenterValue"),
    spineLabel: document.querySelector("#cubeSpineLabel"),
    spineHeadline: document.querySelector("#cubeSpineHeadline"),
    spineText: document.querySelector("#cubeSpineText"),
    primaryButton: document.querySelector("#primaryCubeButton"),
    whiteName: document.querySelector("#whiteNameCube"),
    blackName: document.querySelector("#blackNameCube"),
    whiteButton: document.querySelector("#whiteCubeButton"),
    blackButton: document.querySelector("#blackCubeButton"),
    whiteResult: document.querySelector("#whiteCubeResult"),
    blackResult: document.querySelector("#blackCubeResult"),
    whiteCap: document.querySelector("#whiteCubeCap"),
    blackCap: document.querySelector("#blackCubeCap"),
    whiteHint: document.querySelector("#whiteHintCube"),
    blackHint: document.querySelector("#blackHintCube"),
    openHelpButton: document.querySelector("#openCubeHelpButton"),
    closeHelpButton: document.querySelector("#closeCubeHelpButton"),
    helpModal: document.querySelector("#cubeHelpModal"),
    helpStatus: document.querySelector("#cubeHelpStatus"),
    resultModal: document.querySelector("#cubeResultModal"),
    closeResultButton: document.querySelector("#closeCubeResultButton"),
    resultModalTitle: document.querySelector("#cubeResultModalTitle"),
    resultSummary: document.querySelector("#cubeResultSummary"),
    resultWinner: document.querySelector("#cubeResultWinner"),
    resultOutcome: document.querySelector("#cubeResultOutcome"),
    resultWhiteName: document.querySelector("#cubeResultWhiteName"),
    resultBlackName: document.querySelector("#cubeResultBlackName"),
    resultWhiteTime: document.querySelector("#cubeResultWhiteTime"),
    resultBlackTime: document.querySelector("#cubeResultBlackTime"),
    resultWhiteDetail: document.querySelector("#cubeResultWhiteDetail"),
    resultBlackDetail: document.querySelector("#cubeResultBlackDetail"),
    resultWhiteClock: document.querySelector("#cubeResultWhiteClock"),
    resultBlackClock: document.querySelector("#cubeResultBlackClock"),
    resultActionButton: document.querySelector("#cubeResultActionButton"),
    resultDismissButton: document.querySelector("#cubeResultDismissButton"),
    replayCubeButton: document.querySelector("#replayCubeButton"),
    resetButton: document.querySelector("#cubeResetButton"),
  };
  const cubeHoldState = {
    white: createCubeHoldIntent(),
    black: createCubeHoldIntent(),
  };
  let cubeHoldAnimationFrameId = null;
  let resultModalKey = null;

  const openModal = () => toggleModal(refs.helpModal, true);
  const closeModal = () => toggleModal(refs.helpModal, false);
  const openResultModal = () => toggleModal(refs.resultModal, true);
  const closeResultModal = () => toggleModal(refs.resultModal, false);

  refs.openHelpButton?.addEventListener("click", openModal);
  refs.closeHelpButton?.addEventListener("click", closeModal);
  refs.helpModal?.addEventListener("click", (event) => {
    const target = event.target;
    if (target instanceof HTMLElement && target.dataset.closeCubeModal === "true") {
      closeModal();
    }
  });
  refs.closeResultButton?.addEventListener("click", closeResultModal);
  refs.resultDismissButton?.addEventListener("click", closeResultModal);
  refs.resultModal?.addEventListener("click", (event) => {
    const target = event.target;
    if (target instanceof HTMLElement && target.dataset.closeCubeResultModal === "true") {
      closeResultModal();
    }
  });

  bindCubeButton("white", refs.whiteButton);
  bindCubeButton("black", refs.blackButton);

  refs.primaryButton?.addEventListener("click", () => {
    if (match.result) {
      navigateTo(SETUP_PAGE);
      return;
    }

    if (match.phase !== "cube") {
      navigateTo("chrono.html");
      return;
    }

    if (match.cube.times.white !== null && match.cube.times.black !== null) {
      openResultModal();
    }
  });

  refs.resultActionButton?.addEventListener("click", () => {
    if (!match || match.result || match.phase !== "cube") {
      closeResultModal();
      return;
    }

    if (match.cube.times.white === null || match.cube.times.black === null) {
      closeResultModal();
      return;
    }

    if (match.config.mode === "twice" && match.cube.times.white === match.cube.times.black) {
      closeResultModal();
      resultModalKey = null;
      replayCubePhase(match);
      dirty = true;
      maybePlayPendingCubePhaseAlert(match);
      render();
      return;
    }

    closeResultModal();
    applyCubeOutcome(match);
    dirty = true;
    persistMatch();
    navigateTo("chrono.html");
  });

  refs.replayCubeButton?.addEventListener("click", () => {
    closeResultModal();
    resultModalKey = null;
    replayCubePhase(match);
    dirty = true;
    maybePlayPendingCubePhaseAlert(match);
    render();
  });

  refs.resetButton?.addEventListener("click", () => {
    closeResultModal();
    clearMatch();
    replaceTo(SETUP_PAGE);
  });

  function bindCubeButton(color, button) {
    if (!button) {
      return;
    }

    button.addEventListener("pointerdown", (event) => {
      handleCubePressStart(color, button, event);
    });

    button.addEventListener("pointerup", (event) => {
      handleCubePressEnd(color, button, event);
    });

    button.addEventListener("pointercancel", () => {
      cancelCubeHold(color);
      render();
    });

    button.addEventListener("lostpointercapture", () => {
      cancelCubeHold(color);
      render();
    });

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

    button.addEventListener("click", (event) => {
      if (event.detail !== 0) {
        event.preventDefault();
        return;
      }

      handleCubeKeyboardAction(color);
      dirty = true;
      render();
    });
  }

  function handleCubePressStart(color, button, event) {
    if (match.result || match.phase !== "cube") {
      return;
    }

    if (match.cube.times[color] !== null) {
      return;
    }

    if (match.cube.playerState[color].running) {
      return;
    }

    const holdIntent = cubeHoldState[color];
    clearCubeHoldTimeout(holdIntent);
    holdIntent.armed = true;
    holdIntent.ready = false;
    holdIntent.startedAt = Date.now();
    holdIntent.pointerId = event.pointerId;
    holdIntent.timeoutId = window.setTimeout(() => {
      holdIntent.timeoutId = null;
      if (!holdIntent.armed) {
        return;
      }

      holdIntent.ready = true;
      render();
    }, CUBE_START_HOLD_MS);

    try {
      button.setPointerCapture(event.pointerId);
    } catch {
      // Ignore browsers that do not support pointer capture on buttons.
    }

    render();
    ensureCubeHoldAnimation();
  }

  function handleCubePressEnd(color, button, event) {
    if (match.result || match.phase !== "cube") {
      cancelCubeHold(color);
      render();
      return;
    }

    if (match.cube.times[color] !== null) {
      cancelCubeHold(color);
      render();
      return;
    }

    if (match.cube.playerState[color].running) {
      captureCubeTime(match, color);
      dirty = true;
      render();
      return;
    }

    const holdIntent = cubeHoldState[color];
    const wasReady = holdIntent.pointerId === event.pointerId && holdIntent.ready;
    cancelCubeHold(color);

    try {
      button.releasePointerCapture(event.pointerId);
    } catch {
      // Ignore browsers that already released the capture.
    }

    if (!wasReady) {
      render();
      return;
    }

    startCubeTimer(match, color);
    dirty = true;
    render();
  }

  function handleCubeKeyboardAction(color) {
    if (match.result || match.phase !== "cube" || match.cube.times[color] !== null) {
      return;
    }

    if (match.cube.playerState[color].running) {
      captureCubeTime(match, color);
      return;
    }

    startCubeTimer(match, color);
  }

  function cancelCubeHold(color) {
    const holdIntent = cubeHoldState[color];
    clearCubeHoldTimeout(holdIntent);
    holdIntent.armed = false;
    holdIntent.ready = false;
    holdIntent.startedAt = 0;
    holdIntent.pointerId = null;
    stopCubeHoldAnimationIfIdle();
  }

  function ensureCubeHoldAnimation() {
    if (cubeHoldAnimationFrameId !== null) {
      return;
    }

    const tick = () => {
      cubeHoldAnimationFrameId = null;
      if (!isCubeHoldAnimating()) {
        render();
        return;
      }

      render();
      cubeHoldAnimationFrameId = window.requestAnimationFrame(tick);
    };

    cubeHoldAnimationFrameId = window.requestAnimationFrame(tick);
  }

  function stopCubeHoldAnimationIfIdle() {
    if (cubeHoldAnimationFrameId === null || isCubeHoldAnimating()) {
      return;
    }

    window.cancelAnimationFrame(cubeHoldAnimationFrameId);
    cubeHoldAnimationFrameId = null;
  }

  function isCubeHoldAnimating() {
    return Object.entries(cubeHoldState).some(([color, holdIntent]) => {
      const playerState = match.cube.playerState[color];
      return holdIntent.armed && !holdIntent.ready && !playerState.running && match.cube.times[color] === null;
    });
  }

  function clearCubeHoldTimeout(holdIntent) {
    if (holdIntent.timeoutId !== null) {
      window.clearTimeout(holdIntent.timeoutId);
      holdIntent.timeoutId = null;
    }
  }

  function renderCubeZone(color) {
    const isWhite = color === "white";
    const button = isWhite ? refs.whiteButton : refs.blackButton;
    const name = isWhite ? refs.whiteName : refs.blackName;
    const result = isWhite ? refs.whiteResult : refs.blackResult;
    const cap = isWhite ? refs.whiteCap : refs.blackCap;
    const hint = isWhite ? refs.whiteHint : refs.blackHint;
    const playerState = match.cube.playerState[color];
    const holdIntent = cubeHoldState[color];
    const time = match.cube.times[color];
    const holdArmed = holdIntent.armed && !playerState.running && time === null && match.phase === "cube" && !match.result;
    const holdReady = holdIntent.ready && holdArmed;
    const holdProgress = holdArmed
      ? Math.min((Date.now() - holdIntent.startedAt) / CUBE_START_HOLD_MS, 1)
      : 0;

    name.textContent = playerName(match, color);
    result.textContent = formatCubePlayerTime(match, color);
    cap.textContent = renderCubeMeta(match, color);
    button.classList.toggle("cube-hold-arming", holdArmed && !holdReady);
    button.classList.toggle("cube-hold-ready", holdReady);
    button.style.setProperty("--cube-hold-progress", `${holdProgress}`);

    if (match.result) {
      button.textContent = resultText(match);
      button.disabled = true;
      hint.textContent = "Le match est termine.";
      return;
    }

    if (match.phase !== "cube") {
      button.textContent = "Retour chrono";
      button.disabled = true;
      hint.textContent = "La page cube est terminee.";
      return;
    }

    if (time !== null) {
      button.textContent = "Temps enregistre";
      button.disabled = true;
      hint.textContent = "Ce joueur a deja termine son cube.";
      return;
    }

    if (playerState.running) {
      button.textContent = "J'ai fini le cube";
      button.disabled = false;
      hint.textContent = "Tape au moment exact ou le cube est resolu.";
      return;
    }

    if (holdReady) {
      button.textContent = "Relachez pour demarrer";
      button.disabled = false;
      hint.textContent = "Le chrono partira des que vous levez le doigt.";
      return;
    }

    if (holdArmed) {
      button.textContent = "Maintenez 2 s...";
      button.disabled = false;
      hint.textContent = "Gardez le doigt pose 2 secondes, jusqu'a la fin de la barre.";
      return;
    }

    button.textContent = "Maintenir 2 s pour demarrer";
    button.disabled = false;
    hint.textContent = "Maintenez la grande zone 2 secondes, puis relachez pour lancer votre chrono.";
  }

  function getCubeResultState() {
    if (!match || match.phase !== "cube") {
      return null;
    }

    const white = match.cube.times.white;
    const black = match.cube.times.black;
    if (white === null || black === null) {
      return null;
    }

    const whiteName = playerName(match, "white");
    const blackName = playerName(match, "black");

    if (match.config.mode === "time") {
      const preview = getTimeAdjustmentPreview(match, white, black);
      if (!preview) {
        return null;
      }

      return {
        key: `time:${match.blockNumber}:${match.cube.round}:${white}:${black}`,
        title: "Résumé du cube",
        winner: preview.winner ? playerName(match, preview.winner) : "Egalite",
        outcome:
          preview.blockType === "minus" ? "Bloc - a appliquer" : "Bloc + a appliquer",
        summary:
          "Validez ce résumé pour appliquer les impacts chrono puis revenir à la page chrono.",
        actionLabel: "Appliquer et ouvrir la page chrono",
        whiteName,
        blackName,
        whiteTime: `Temps cube ${formatStopwatch(white)}`,
        blackTime: `Temps cube ${formatStopwatch(black)}`,
        whiteDetail: `Impact chrono ${formatSignedStopwatch(preview.whiteDelta)}`,
        blackDetail: `Impact chrono ${formatSignedStopwatch(preview.blackDelta)}`,
        whiteClock: `Chrono apres ${formatSignedClock(preview.whiteAfter)}`,
        blackClock: `Chrono apres ${formatSignedClock(preview.blackAfter)}`,
      };
    }

    const winner = white < black ? "white" : black < white ? "black" : null;
    const isTie = winner === null;

    return {
      key: `twice:${match.blockNumber}:${match.cube.round}:${white}:${black}`,
      title: isTie ? "Egalite parfaite" : "Résumé du cube",
      winner: winner ? playerName(match, winner) : "Egalite parfaite",
      outcome: isTie ? "Rejouer la phase cube" : `${playerName(match, winner)} ouvrira le Block suivant`,
      summary: isTie
        ? "Le règlement Twice impose de rejouer immédiatement la phase cube."
        : "Validez ce résultat pour preparer le Block suivant.",
      actionLabel: isTie ? "Rejouer la phase cube" : "Appliquer et ouvrir la page chrono",
      whiteName,
      blackName,
      whiteTime: `Temps cube ${formatStopwatch(white)}`,
      blackTime: `Temps cube ${formatStopwatch(black)}`,
      whiteDetail: winner === "white" ? "Gagne la phase cube" : isTie ? "Egalite parfaite" : "Ne gagne pas la phase cube",
      blackDetail: winner === "black" ? "Gagne la phase cube" : isTie ? "Egalite parfaite" : "Ne gagne pas la phase cube",
      whiteClock: "Aucun impact chrono en mode Twice",
      blackClock: "Aucun impact chrono en mode Twice",
    };
  }

  function renderResultModal() {
    const resultState = getCubeResultState();
    if (!resultState) {
      closeResultModal();
      resultModalKey = null;
      return;
    }

    refs.resultModalTitle.textContent = resultState.title;
    refs.resultSummary.textContent = resultState.summary;
    refs.resultWinner.textContent = resultState.winner;
    refs.resultOutcome.textContent = resultState.outcome;
    refs.resultWhiteName.textContent = resultState.whiteName;
    refs.resultBlackName.textContent = resultState.blackName;
    refs.resultWhiteTime.textContent = resultState.whiteTime;
    refs.resultBlackTime.textContent = resultState.blackTime;
    refs.resultWhiteDetail.textContent = resultState.whiteDetail;
    refs.resultBlackDetail.textContent = resultState.blackDetail;
    refs.resultWhiteClock.textContent = resultState.whiteClock;
    refs.resultBlackClock.textContent = resultState.blackClock;
    refs.resultActionButton.textContent = resultState.actionLabel;

    if (resultState.key !== resultModalKey) {
      resultModalKey = resultState.key;
      openResultModal();
    }
  }

  function render() {
    const blockHeading = formatBlockHeading(match, match.blockNumber);
    const timePreview =
      isTimeMode(match) &&
      match.cube.times.white !== null &&
      match.cube.times.black !== null
        ? getTimeAdjustmentPreview(match, match.cube.times.white, match.cube.times.black)
        : null;

    refs.title.textContent = match.cube.number ? `Cube n${match.cube.number}` : "Phase cube";
    refs.subtitle.textContent = `${blockHeading} - ${MODES[match.config.mode].label} - ${renderModeContext(match)}`;
    refs.blockLabelText.textContent = getBlockLabel(match);
    refs.blockLabel.textContent = `${match.blockNumber}`;
    refs.elapsed.textContent = renderCubeElapsed(match);

    if (match.result) {
      refs.centerLabel.textContent = "Resultat";
      refs.centerValue.textContent = resultText(match);
      refs.spineLabel.textContent = "Termine";
      refs.spineHeadline.textContent = resultText(match);
      refs.spineText.textContent = "Retournez a la configuration pour relancer une rencontre.";
      refs.primaryButton.textContent = "Retour a l'accueil";
      refs.helpStatus.textContent = "Le match est termine.";
    } else if (
      match.cube.times.white !== null &&
      match.cube.times.black !== null &&
      match.config.mode === "twice" &&
      match.cube.times.white === match.cube.times.black
    ) {
      refs.centerLabel.textContent = "Decision";
      refs.centerValue.textContent = "Egalite parfaite";
      refs.spineLabel.textContent = "Reglement";
      refs.spineHeadline.textContent = "Rejouer la phase cube";
      refs.spineText.textContent = "Le mode Twice impose de relancer immediatement la phase cube en cas d'egalite parfaite.";
      refs.primaryButton.textContent = "Voir le résumé du cube";
      refs.helpStatus.textContent = refs.spineText.textContent;
    } else if (match.cube.times.white !== null && match.cube.times.black !== null) {
      if (timePreview) {
        refs.centerLabel.textContent = "Vainqueur cube";
        refs.centerValue.textContent = timePreview.winner
          ? playerName(match, timePreview.winner)
          : "Egalite";
        refs.spineLabel.textContent = "Impact chrono";
        refs.spineHeadline.textContent =
          timePreview.blockType === "minus" ? "Bloc - a appliquer" : "Bloc + a appliquer";
        refs.spineText.textContent =
          `Blanc ${formatSignedStopwatch(timePreview.whiteDelta)} -> ${formatSignedClock(timePreview.whiteAfter)}. ` +
          `Noir ${formatSignedStopwatch(timePreview.blackDelta)} -> ${formatSignedClock(timePreview.blackAfter)}.`;
      } else {
        refs.centerLabel.textContent = "Decision";
        refs.centerValue.textContent = "Phase cube complete";
        refs.spineLabel.textContent = "Suite";
        refs.spineHeadline.textContent = "Ouvrir la page chrono";
        refs.spineText.textContent = "Appliquer le resultat du cube pour preparer le Block suivant.";
      }
      refs.primaryButton.textContent = "Voir le résumé du cube";
      refs.helpStatus.textContent = refs.spineText.textContent;
    } else if (match.cube.running) {
      refs.centerLabel.textContent = "Etat";
      refs.centerValue.textContent = "Chronos lances";
      refs.spineLabel.textContent = "Arrets";
      refs.spineHeadline.textContent = "Chaque joueur se chronometre";
      refs.spineText.textContent = "Chaque joueur demarre en relachant sa zone, puis retape sa zone une fois le cube termine.";
      refs.primaryButton.textContent = "Attendre les deux temps";
      refs.helpStatus.textContent = refs.spineText.textContent;
    } else if (match.cube.times.white !== null || match.cube.times.black !== null) {
      refs.centerLabel.textContent = "Etat";
      refs.centerValue.textContent = "Un temps saisi";
      refs.spineLabel.textContent = "Suite";
      refs.spineHeadline.textContent = "Attendre l'autre joueur";
      refs.spineText.textContent = "Le deuxieme joueur peut encore maintenir puis relacher sa zone pour demarrer son propre chrono.";
      refs.primaryButton.textContent = "Attendre le deuxieme temps";
      refs.helpStatus.textContent = refs.spineText.textContent;
    } else {
      refs.centerLabel.textContent = "Etat";
      refs.centerValue.textContent = "Pret";
      refs.spineLabel.textContent = "Depart libre";
      refs.spineHeadline.textContent = "Chaque joueur lance son chrono";
      refs.spineText.textContent = "Au debut de sa resolution, chaque joueur maintient sa grande zone puis la relache pour demarrer son propre chrono.";
      refs.primaryButton.textContent = "En attente des joueurs";
      refs.helpStatus.textContent = refs.spineText.textContent;
    }

    renderCubeZone("black");
    renderCubeZone("white");
    renderResultModal();
    refs.primaryButton.disabled =
      !match.result &&
      !(
        match.cube.times.white !== null &&
        match.cube.times.black !== null
      );
  }

  render();

  window.setInterval(() => {
    if (!match) {
      return;
    }

    if (!match.result && match.phase !== "cube") {
      persistMatch();
      navigateTo("chrono.html");
      return;
    }

    render();
  }, 100);
}

function createMatch(config) {
  const quota = PRESETS[config.preset].quota;
  const newMatch = {
    schemaVersion: 3,
    config,
    phase: "block",
    running: false,
    lastTickAt: null,
    blockNumber: 1,
    currentTurn: "white",
    blockRemainingMs: config.blockDurationMs,
    moveRemainingMs: config.moveLimitMs,
    quota,
    moves: {
      white: 0,
      black: 0,
    },
    clocks:
      config.mode === "time"
        ? {
            white: getTimeInitialMs(config),
            black: getTimeInitialMs(config),
          }
        : null,
    lastMover: null,
    awaitingBlockClosure: false,
    closureReason: "",
    result: null,
    cube: {
      number: null,
      running: false,
      startedAt: null,
      elapsedMs: 0,
      phaseAlertPending: false,
      times: {
        white: null,
        black: null,
      },
      playerState: {
        white: createCubePlayerState(),
        black: createCubePlayerState(),
      },
      round: 1,
      history: [],
    },
    doubleCoup: {
      eligible: false,
      step: 0,
      starter: "white",
    },
    history: [],
  };

  logEvent(
    newMatch,
    usesMoveLimit(config.mode)
      ? `Match cree en mode ${MODES[config.mode].label}, cadence ${PRESETS[config.preset].label}, Block ${formatClock(config.blockDurationMs)} et coup ${formatClock(config.moveLimitMs)}.`
      : `Match cree en mode ${MODES[config.mode].label}, cadence ${PRESETS[config.preset].label}, Block ${formatClock(config.blockDurationMs)} et chrono initial ${formatClock(getTimeInitialMs(config))} par joueur, sans temps par coup.`,
  );
  logEvent(newMatch, `Les Blancs commencent ${formatBlockHeading(config, 1)}.`);
  return newMatch;
}

function readStoredMatch() {
  const fromWindowName = readWindowNameState();

  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) {
      return fromWindowName;
    }

    const parsed = JSON.parse(raw);
    if (!parsed || !isSupportedSchemaVersion(parsed.schemaVersion)) {
      return fromWindowName;
    }

    return parsed;
  } catch {
    return fromWindowName;
  }
}

function normalizeRecoveredMatch(storedMatch) {
  let changed = false;

  if (!storedMatch.config) {
    storedMatch.config = {};
    changed = true;
  }

  const blockDurationMs = getBlockDurationMs(storedMatch);
  const moveLimitMs = getMoveLimitMs(storedMatch);
  const timeInitialMs = getTimeInitialMs(storedMatch);

  if (storedMatch.schemaVersion !== 3) {
    storedMatch.schemaVersion = 3;
    changed = true;
  }

  if (storedMatch.config.blockDurationMs !== blockDurationMs) {
    storedMatch.config.blockDurationMs = blockDurationMs;
    changed = true;
  }

  if (storedMatch.config.moveLimitMs !== moveLimitMs) {
    storedMatch.config.moveLimitMs = moveLimitMs;
    changed = true;
  }

  if (storedMatch.config.timeInitialMs !== timeInitialMs) {
    storedMatch.config.timeInitialMs = timeInitialMs;
    changed = true;
  }

  if (storedMatch.phase === "block" && typeof storedMatch.lastTickAt !== "number") {
    storedMatch.lastTickAt = null;
    changed = true;
  }

  if (typeof storedMatch.blockRemainingMs !== "number") {
    storedMatch.blockRemainingMs = blockDurationMs;
    changed = true;
  }

  if (typeof storedMatch.moveRemainingMs !== "number") {
    storedMatch.moveRemainingMs = moveLimitMs;
    changed = true;
  }

  if (!storedMatch.cube) {
    storedMatch.cube = {
      number: null,
      running: false,
      startedAt: null,
      elapsedMs: 0,
      phaseAlertPending: false,
      times: { white: null, black: null },
      playerState: {
        white: createCubePlayerState(),
        black: createCubePlayerState(),
      },
      round: 1,
      history: [],
    };
    changed = true;
  }

  if (!storedMatch.cube.playerState) {
    storedMatch.cube.playerState = {
      white: createCubePlayerState(),
      black: createCubePlayerState(),
    };
    changed = true;
  }

  if (typeof storedMatch.cube.phaseAlertPending !== "boolean") {
    storedMatch.cube.phaseAlertPending = false;
    changed = true;
  }

  storedMatch.cube.playerState.white = normalizeCubePlayerState(storedMatch.cube.playerState.white);
  storedMatch.cube.playerState.black = normalizeCubePlayerState(storedMatch.cube.playerState.black);
  storedMatch.cube.running = isAnyCubeTimerRunning(storedMatch);

  if (!storedMatch.doubleCoup) {
    storedMatch.doubleCoup = {
      eligible: false,
      step: 0,
      starter: "white",
    };
    changed = true;
  }

  if (storedMatch.awaitingBlockClosure && storedMatch.phase === "block") {
    openCubePhase(storedMatch, storedMatch.closureReason || "La phase chess etait deja terminee.");
    changed = true;
  }

  return changed;
}

function syncRunningState(storedMatch) {
  if (!storedMatch || storedMatch.result) {
    return false;
  }

  if (!storedMatch.running || storedMatch.phase !== "block" || !storedMatch.lastTickAt) {
    return false;
  }

  const now = Date.now();
  const delta = now - storedMatch.lastTickAt;
  if (delta <= 0) {
    return false;
  }

  storedMatch.lastTickAt = now;
  storedMatch.blockRemainingMs = Math.max(0, storedMatch.blockRemainingMs - delta);
  if (usesMoveLimit(storedMatch)) {
    storedMatch.moveRemainingMs = Math.max(0, storedMatch.moveRemainingMs - delta);
  }

  if (storedMatch.clocks) {
    storedMatch.clocks[storedMatch.currentTurn] -= delta;
  }

  if (storedMatch.blockRemainingMs === 0) {
    requestBlockClosure(
      storedMatch,
      `Le temps ${getBlockGenitivePhrase(storedMatch)} ${formatClock(getBlockDurationMs(storedMatch))} est ecoule.`,
    );
  } else if (usesMoveLimit(storedMatch) && storedMatch.moveRemainingMs === 0) {
    registerMoveTimeout(storedMatch, true);
  }

  return true;
}

function startBlock(storedMatch) {
  if (!storedMatch || storedMatch.phase !== "block" || storedMatch.result) {
    return;
  }

  storedMatch.running = true;
  storedMatch.awaitingBlockClosure = false;
  storedMatch.closureReason = "";
  storedMatch.lastTickAt = Date.now();
  logEvent(
    storedMatch,
    storedMatch.blockNumber === 1 && storedMatch.moves.white === 0 && storedMatch.moves.black === 0
      ? `${formatBlockHeading(storedMatch, 1)} demarre.`
      : `${formatBlockHeading(storedMatch, storedMatch.blockNumber)} relance.`,
  );
}

function pauseBlock(storedMatch) {
  if (!storedMatch || !storedMatch.running) {
    return;
  }

  storedMatch.running = false;
  storedMatch.lastTickAt = null;
  logEvent(storedMatch, `${formatBlockHeading(storedMatch, storedMatch.blockNumber)} passe en pause.`);
}

function requestBlockClosure(storedMatch, reason) {
  if (!storedMatch || storedMatch.phase !== "block") {
    return;
  }

  storedMatch.running = false;
  storedMatch.lastTickAt = null;
  storedMatch.awaitingBlockClosure = false;
  storedMatch.closureReason = "";
  storedMatch.moveRemainingMs = getMoveLimitMs(storedMatch);
  logEvent(storedMatch, `${reason} Passage automatique vers la page cube.`);
  openCubePhase(storedMatch, reason);
}

function openCubePhase(storedMatch, reason = "") {
  if (!storedMatch) {
    return;
  }

  storedMatch.phase = "cube";
  storedMatch.running = false;
  storedMatch.lastTickAt = null;
  storedMatch.awaitingBlockClosure = false;
  storedMatch.closureReason = "";
  storedMatch.cube.number = pickCubeNumber();
  storedMatch.cube.running = false;
  storedMatch.cube.startedAt = null;
  storedMatch.cube.elapsedMs = 0;
  storedMatch.cube.phaseAlertPending = true;
  storedMatch.cube.times = { white: null, black: null };
  storedMatch.cube.playerState = {
    white: createCubePlayerState(),
    black: createCubePlayerState(),
  };
  storedMatch.cube.round = 1;
  logEvent(
    storedMatch,
    `${reason ? `${reason} ` : ""}Page cube ouverte. Cube n${storedMatch.cube.number} designe.`,
  );
}

function startCubeTimer(storedMatch, color) {
  if (!storedMatch || storedMatch.phase !== "cube" || storedMatch.result) {
    return;
  }

  if (!storedMatch.cube.number) {
    storedMatch.cube.number = pickCubeNumber();
  }

  if (storedMatch.cube.times[color] !== null || storedMatch.cube.playerState[color].running) {
    return;
  }

  const playerState = storedMatch.cube.playerState[color];
  playerState.running = true;
  playerState.startedAt = Date.now();
  storedMatch.cube.running = true;
  logEvent(
    storedMatch,
    `${playerName(storedMatch, color)} demarre son chrono cube sur le cube n${storedMatch.cube.number}.`,
  );
}

function captureCubeTime(storedMatch, color) {
  if (
    !storedMatch ||
    storedMatch.phase !== "cube" ||
    !storedMatch.cube.playerState[color].running ||
    storedMatch.cube.times[color] !== null
  ) {
    return;
  }

  const playerState = storedMatch.cube.playerState[color];
  const elapsedMs = playerState.elapsedMs + (Date.now() - playerState.startedAt);
  storedMatch.cube.times[color] = elapsedMs;
  playerState.elapsedMs = elapsedMs;
  playerState.startedAt = null;
  playerState.running = false;
  storedMatch.cube.elapsedMs = Math.max(
    getCubePlayerElapsed(storedMatch, "white"),
    getCubePlayerElapsed(storedMatch, "black"),
  );
  storedMatch.cube.running = isAnyCubeTimerRunning(storedMatch);
  logEvent(storedMatch, `${playerName(storedMatch, color)} arrete le cube en ${formatStopwatch(elapsedMs)}.`);

  if (
    storedMatch.cube.times.white !== null &&
    storedMatch.cube.times.black !== null
  ) {
    storedMatch.cube.running = false;
    storedMatch.cube.history.push({
      blockNumber: storedMatch.blockNumber,
      number: storedMatch.cube.number,
      white: storedMatch.cube.times.white,
      black: storedMatch.cube.times.black,
    });
  }
}

function applyCubeOutcome(storedMatch) {
  if (!storedMatch || storedMatch.phase !== "cube") {
    return;
  }

  const white = storedMatch.cube.times.white;
  const black = storedMatch.cube.times.black;

  if (white === null || black === null) {
    return;
  }

  if (storedMatch.config.mode === "twice") {
    const winner = white < black ? "white" : "black";
    prepareNextTwiceBlock(storedMatch, winner);
    return;
  }

  applyTimeAdjustments(storedMatch, white, black);
  prepareNextTimeBlock(storedMatch);
}

function prepareNextTwiceBlock(storedMatch, winner) {
  const hadDouble = storedMatch.lastMover !== winner && storedMatch.lastMover !== null;
  logEvent(storedMatch, `${playerName(storedMatch, winner)} gagne la phase cube et ouvrira le Block suivant.`);

  storedMatch.blockNumber += 1;
  storedMatch.phase = "block";
  storedMatch.running = false;
  storedMatch.lastTickAt = null;
  storedMatch.blockRemainingMs = getBlockDurationMs(storedMatch);
  storedMatch.moveRemainingMs = getMoveLimitMs(storedMatch);
  storedMatch.moves = { white: 0, black: 0 };
  storedMatch.currentTurn = winner;
  storedMatch.doubleCoup = {
    eligible: hadDouble,
    step: hadDouble ? 1 : 0,
    starter: winner,
  };
  storedMatch.cube.running = false;
  storedMatch.cube.startedAt = null;
  storedMatch.cube.elapsedMs = 0;
  storedMatch.cube.times = { white: null, black: null };
  storedMatch.cube.playerState = {
    white: createCubePlayerState(),
    black: createCubePlayerState(),
  };
  storedMatch.cube.number = null;

  if (hadDouble) {
    logEvent(storedMatch, `Double coup disponible pour ${playerName(storedMatch, winner)}.`);
  } else {
    logEvent(storedMatch, "Aucun double coup n'est accorde sur ce depart.");
  }
}

function prepareNextTimeBlock(storedMatch) {
  storedMatch.blockNumber += 1;
  storedMatch.phase = "block";
  storedMatch.running = false;
  storedMatch.lastTickAt = null;
  storedMatch.blockRemainingMs = getBlockDurationMs(storedMatch);
  storedMatch.moveRemainingMs = getMoveLimitMs(storedMatch);
  storedMatch.moves = { white: 0, black: 0 };
  storedMatch.doubleCoup = {
    eligible: false,
    step: 0,
    starter: storedMatch.currentTurn,
  };
  storedMatch.cube.running = false;
  storedMatch.cube.startedAt = null;
  storedMatch.cube.elapsedMs = 0;
  storedMatch.cube.times = { white: null, black: null };
  storedMatch.cube.playerState = {
    white: createCubePlayerState(),
    black: createCubePlayerState(),
  };
  storedMatch.cube.number = null;
  logEvent(
    storedMatch,
    `${formatBlockHeading(storedMatch, storedMatch.blockNumber)} pret. Le trait est conserve : ${playerName(
      storedMatch,
      storedMatch.currentTurn,
    )} reprend.`,
  );
}

function applyTimeAdjustments(storedMatch, whiteTime, blackTime) {
  const preview = getTimeAdjustmentPreview(storedMatch, whiteTime, blackTime);
  if (!preview) {
    return;
  }

  storedMatch.clocks.white = preview.whiteAfter;
  storedMatch.clocks.black = preview.blackAfter;

  if (preview.blockType === "minus") {
    logEvent(
      storedMatch,
      `Bloc - : ${formatStopwatch(preview.cappedWhite)} retire au chrono Blanc, ${formatStopwatch(
        preview.cappedBlack,
      )} retire au chrono Noir.`,
    );
  } else {
    logEvent(
      storedMatch,
      `Bloc + : ${formatStopwatch(preview.cappedBlack)} ajoute au chrono Blanc, ${formatStopwatch(
        preview.cappedWhite,
      )} ajoute au chrono Noir.`,
    );
  }
}

function replayCubePhase(storedMatch) {
  if (!storedMatch || storedMatch.phase !== "cube") {
    return;
  }

  storedMatch.cube.running = false;
  storedMatch.cube.startedAt = null;
  storedMatch.cube.elapsedMs = 0;
  storedMatch.cube.phaseAlertPending = true;
  storedMatch.cube.times = { white: null, black: null };
  storedMatch.cube.playerState = {
    white: createCubePlayerState(),
    black: createCubePlayerState(),
  };
  storedMatch.cube.round += 1;
  logEvent(storedMatch, `Phase cube relancee (tentative ${storedMatch.cube.round}).`);
}

function registerCountedMove(storedMatch, { source }) {
  if (!storedMatch || storedMatch.phase !== "block") {
    return;
  }

  const actor = storedMatch.currentTurn;
  if (storedMatch.moves[actor] >= storedMatch.quota) {
    logEvent(
      storedMatch,
      `${playerName(storedMatch, actor)} a deja atteint son quota. Utiliser le mode hors quota si necessaire.`,
    );
    return;
  }

  storedMatch.moves[actor] += 1;
  storedMatch.lastMover = actor;
  storedMatch.moveRemainingMs = getMoveLimitMs(storedMatch);

  if (source === "double") {
    storedMatch.doubleCoup.step = 0;
  }

  logEvent(
    storedMatch,
    `${playerName(storedMatch, actor)} valide son coup (${storedMatch.moves[actor]} / ${storedMatch.quota}).`,
  );

  storedMatch.currentTurn = opponentOf(actor);
  verifyQuotaCompletion(storedMatch);
}

function registerFreeDoubleMove(storedMatch) {
  if (!storedMatch || storedMatch.doubleCoup.step !== 1) {
    return;
  }

  const actor = storedMatch.currentTurn;
  storedMatch.lastMover = actor;
  storedMatch.moveRemainingMs = getMoveLimitMs(storedMatch);
  storedMatch.doubleCoup.step = 2;
  logEvent(
    storedMatch,
    `Premier coup gratuit du double coup joue par ${playerName(storedMatch, actor)}.`,
  );
}

function registerMoveTimeout(storedMatch, automatic) {
  if (!storedMatch || storedMatch.phase !== "block" || !usesMoveLimit(storedMatch)) {
    return;
  }

  const actor = storedMatch.currentTurn;

  if (storedMatch.doubleCoup.step === 1) {
    storedMatch.doubleCoup.step = 0;
    storedMatch.moveRemainingMs = getMoveLimitMs(storedMatch);
    logEvent(
      storedMatch,
      `Depassement sur le premier coup gratuit de ${playerName(storedMatch, actor)} : le double coup est annule.`,
    );
    return;
  }

  if (storedMatch.moves[actor] < storedMatch.quota) {
    storedMatch.moves[actor] += 1;
  }

  if (storedMatch.doubleCoup.step === 2) {
    storedMatch.doubleCoup.step = 0;
  }

  storedMatch.currentTurn = opponentOf(actor);
  storedMatch.moveRemainingMs = getMoveLimitMs(storedMatch);
  logEvent(
    storedMatch,
    `${automatic ? "Temps par coup ecoule." : `Depassement manuel du temps par coup ${formatClock(getMoveLimitMs(storedMatch))}.`} ${playerName(
      storedMatch,
      actor,
    )} perd son coup, qui reste compte dans le quota.`,
  );
  verifyQuotaCompletion(storedMatch);
}

function verifyQuotaCompletion(storedMatch) {
  if (
    storedMatch.moves.white >= storedMatch.quota &&
    storedMatch.moves.black >= storedMatch.quota
  ) {
    requestBlockClosure(storedMatch, "Les deux joueurs ont atteint leur quota de coups.");
  }
}

function setResult(storedMatch, winner) {
  if (!storedMatch || storedMatch.result) {
    return;
  }

  storedMatch.running = false;
  storedMatch.lastTickAt = null;
  storedMatch.result = winner;
  logEvent(storedMatch, `${resultText(storedMatch)}.`);
}

function renderModeContext(storedMatch) {
  if (storedMatch.config.mode === "time") {
    return getTimeBlockType(storedMatch.blockNumber) === "minus"
      ? "Bloc - : temps cube retire a son propre chrono"
      : "Bloc + : temps cube ajoute au chrono adverse";
  }

  if (storedMatch.doubleCoup.step === 1) {
    return `Double coup actif pour ${playerName(storedMatch, storedMatch.doubleCoup.starter)}`;
  }

  if (storedMatch.doubleCoup.step === 2) {
    return "Deuxieme coup du double en attente";
  }

  return "Le gagnant du cube ouvrira la prochaine partie";
}

function renderLastCube(storedMatch, color) {
  const last = storedMatch.cube.history.at(-1);
  if (!last) {
    return "--";
  }

  return formatStopwatch(last[color]);
}

function renderCubeElapsed(storedMatch) {
  if (storedMatch.phase !== "cube") {
    return "00:00.0";
  }

  return formatStopwatch(
    Math.max(getCubePlayerElapsed(storedMatch, "white"), getCubePlayerElapsed(storedMatch, "black")),
  );
}

function renderCubeMeta(storedMatch, color) {
  const time = storedMatch.cube.times[color];
  if (time === null) {
    return "";
  }

  if (storedMatch.config.mode !== "time") {
    return "temps capture";
  }

  if (
    storedMatch.cube.times.white !== null &&
    storedMatch.cube.times.black !== null
  ) {
    const preview = getTimeAdjustmentPreview(
      storedMatch,
      storedMatch.cube.times.white,
      storedMatch.cube.times.black,
    );
    if (!preview) {
      return "";
    }

    const delta = color === "white" ? preview.whiteDelta : preview.blackDelta;
    const cappedTime = color === "white" ? preview.cappedWhite : preview.cappedBlack;
    const wasCapped = time > cappedTime;
    return wasCapped
      ? `Impact chrono ${formatSignedStopwatch(delta)} (cap ${formatStopwatch(cappedTime)})`
      : `Impact chrono ${formatSignedStopwatch(delta)}`;
  }

  if (time <= CUBE_TIME_CAP_MS) {
    return "impact chrono en attente";
  }

  return `impact en attente, cap ${formatStopwatch(CUBE_TIME_CAP_MS)}`;
}

function resultText(storedMatch) {
  if (storedMatch.result === "white") {
    return `Victoire de ${playerName(storedMatch, "white")}`;
  }

  if (storedMatch.result === "black") {
    return `Victoire de ${playerName(storedMatch, "black")}`;
  }

  return "Partie arretee";
}

function routeForMatch(storedMatch) {
  if (!storedMatch) {
    return SETUP_PAGE;
  }

  return storedMatch.phase === "cube" && !storedMatch.result ? "cube.html" : "chrono.html";
}

function navigateTo(target) {
  persistMatch();
  window.location.href = target;
}

function replaceTo(target) {
  window.location.replace(target);
}

function persistMatch() {
  if (!match) {
    try {
      localStorage.removeItem(STORAGE_KEY);
    } catch {
      // Ignore storage errors and still clear the window-level fallback.
    }
    window.name = "";
    dirty = false;
    return;
  }

  const serialized = JSON.stringify(match);
  window.name = `${WINDOW_NAME_KEY}${serialized}`;

  try {
    localStorage.setItem(STORAGE_KEY, serialized);
  } catch {
    // Keep the window.name fallback so cross-page navigation still works.
  }

  dirty = false;
}

function flushState() {
  if (match) {
    syncRunningState(match);
  }

  if (dirty) {
    persistMatch();
  }
}

function clearMatch() {
  match = null;
  persistMatch();
}

async function forceRefreshToLatest(path = SETUP_PAGE) {
  const refreshToken = `${Date.now()}`;

  try {
    window.sessionStorage.setItem(ASSET_TOKEN_STORAGE_KEY, refreshToken);
  } catch {
    // Ignore storage failures in restricted browsers.
  }

  if ("caches" in window) {
    try {
      const cacheKeys = await window.caches.keys();
      await Promise.all(cacheKeys.map((cacheKey) => window.caches.delete(cacheKey)));
    } catch {
      // Ignore cache API failures when unavailable.
    }
  }

  if ("serviceWorker" in navigator) {
    try {
      const registrations = await navigator.serviceWorker.getRegistrations();
      await Promise.all(registrations.map((registration) => registration.update().catch(() => undefined)));
      await Promise.all(registrations.map((registration) => registration.unregister().catch(() => undefined)));
    } catch {
      // Ignore service worker failures when none are registered.
    }
  }

  const targetUrl = new URL(path, window.location.href);
  targetUrl.searchParams.set("refresh", refreshToken);
  window.location.replace(targetUrl.toString());
}

function toggleModal(element, open) {
  if (!element) {
    return;
  }

  element.classList.toggle("hidden", !open);
  element.setAttribute("aria-hidden", String(!open));
}

function loadDemo(form, onRender) {
  setCheckboxValue(form, "competitionMode", true);
  setInputValue(form, "matchLabel", "Demo officielle ChessCubing");
  setRadioValue(form, "mode", "twice");
  setRadioValue(form, "preset", "freeze");
  setInputValue(form, "blockMinutes", "3");
  setInputValue(form, "moveSeconds", "20");
  setInputValue(form, "timeInitialMinutes", "10");
  setInputValue(form, "whiteName", "Nora");
  setInputValue(form, "blackName", "Leo");
  setInputValue(form, "arbiterName", "Arbitre demo");
  setInputValue(form, "eventName", "Session telephone");
  setInputValue(form, "notes", "8 cubes verifies, variante prete, tirage au sort effectue.");
  onRender();
}

function setInputValue(form, name, value) {
  const input = form.querySelector(`[name="${name}"]`);
  if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
    input.value = value;
  }
}

function getRadioValue(form, name) {
  const selected = form.querySelector(`input[name="${name}"]:checked`);
  return selected ? selected.value : "";
}

function setRadioValue(form, name, value) {
  const input = form.querySelector(`input[name="${name}"][value="${value}"]`);
  if (input) {
    input.checked = true;
  }
}

function setCheckboxValue(form, name, checked) {
  const input = form.querySelector(`input[name="${name}"]`);
  if (input instanceof HTMLInputElement) {
    input.checked = checked;
  }
}

function isCheckboxChecked(form, name) {
  const input = form.querySelector(`input[name="${name}"]`);
  return input instanceof HTMLInputElement ? input.checked : false;
}

function getDurationInputMs(form, name, fallbackMs) {
  const input = form.querySelector(`[name="${name}"]`);
  const seconds = Number.parseInt(String(input?.value || ""), 10);
  if (!Number.isFinite(seconds) || seconds <= 0) {
    return fallbackMs;
  }

  return seconds * 1000;
}

function getMinuteInputMs(form, name, fallbackMs) {
  const input = form.querySelector(`[name="${name}"]`);
  const minutes = Number.parseInt(String(input?.value || ""), 10);
  if (!Number.isFinite(minutes) || minutes <= 0) {
    return fallbackMs;
  }

  return minutes * 60000;
}

function isTimeMode(matchOrConfig) {
  const mode =
    typeof matchOrConfig === "string"
      ? matchOrConfig
      : matchOrConfig?.config?.mode ?? matchOrConfig?.mode;
  return mode === "time";
}

function usesMoveLimit(matchOrConfig) {
  return !isTimeMode(matchOrConfig);
}

function getTimeInitialMs(matchOrConfig) {
  return normalizeDurationMs(
    matchOrConfig?.config?.timeInitialMs ?? matchOrConfig?.timeInitialMs,
    TIME_MODE_INITIAL_CLOCK_MS,
  );
}

function getBlockLabel(matchOrConfig) {
  return "Block";
}

function getBlockPhrase(matchOrConfig) {
  return "Le Block";
}

function getBlockGenitivePhrase(matchOrConfig) {
  return "du Block";
}

function formatBlockHeading(matchOrConfig, blockNumber) {
  return `${getBlockLabel(matchOrConfig)} ${blockNumber}`;
}

function playerName(storedMatch, color) {
  return color === "white" ? storedMatch.config.whiteName : storedMatch.config.blackName;
}

function opponentOf(color) {
  return color === "white" ? "black" : "white";
}

function getTimeBlockType(blockNumber) {
  return blockNumber % 2 === 1 ? "minus" : "plus";
}

function pickCubeNumber() {
  return Math.floor(Math.random() * 4) + 1;
}

function logEvent(storedMatch, message) {
  storedMatch.history.push({
    message,
    time: new Date().toLocaleTimeString("fr-FR", {
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
    }),
  });
}

function formatClock(ms) {
  const totalSeconds = Math.max(0, Math.floor(ms / 1000));
  const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
  const seconds = String(totalSeconds % 60).padStart(2, "0");
  return `${minutes}:${seconds}`;
}

function formatSignedClock(ms) {
  const negative = ms < 0 ? "-" : "";
  const totalSeconds = Math.floor(Math.abs(ms) / 1000);
  const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
  const seconds = String(totalSeconds % 60).padStart(2, "0");
  return `${negative}${minutes}:${seconds}`;
}

function formatSignedStopwatch(ms) {
  const sign = ms < 0 ? "-" : "+";
  return `${sign}${formatStopwatch(ms)}`;
}

function formatStopwatch(ms) {
  const totalSeconds = Math.floor(Math.abs(ms) / 1000);
  const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
  const seconds = String(totalSeconds % 60).padStart(2, "0");
  const tenths = Math.floor((Math.abs(ms) % 1000) / 100);
  return `${minutes}:${seconds}.${tenths}`;
}

function sanitizeText(value) {
  return String(value || "")
    .replace(/[<>]/g, "")
    .trim()
    .replace(/\s+/g, " ");
}

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

function readWindowNameState() {
  try {
    if (!window.name || !window.name.startsWith(WINDOW_NAME_KEY)) {
      return null;
    }

    const raw = window.name.slice(WINDOW_NAME_KEY.length);
    const parsed = JSON.parse(raw);
    return parsed && isSupportedSchemaVersion(parsed.schemaVersion) ? parsed : null;
  } catch {
    return null;
  }
}

function isSupportedSchemaVersion(version) {
  return version === 2 || version === 3;
}

function getBlockDurationMs(storedMatch) {
  return normalizeDurationMs(storedMatch?.config?.blockDurationMs, DEFAULT_BLOCK_DURATION_MS);
}

function getMoveLimitMs(storedMatch) {
  return normalizeDurationMs(storedMatch?.config?.moveLimitMs, DEFAULT_MOVE_LIMIT_MS);
}

function normalizeDurationMs(value, fallbackMs) {
  const duration = Number(value);
  if (!Number.isFinite(duration) || duration <= 0) {
    return fallbackMs;
  }

  return Math.round(duration);
}

function getTimeAdjustmentPreview(storedMatch, whiteTime, blackTime) {
  if (!storedMatch?.clocks) {
    return null;
  }

  const cappedWhite = Math.min(whiteTime, CUBE_TIME_CAP_MS);
  const cappedBlack = Math.min(blackTime, CUBE_TIME_CAP_MS);
  const blockType = getTimeBlockType(storedMatch.blockNumber);
  const whiteDelta = blockType === "minus" ? -cappedWhite : cappedBlack;
  const blackDelta = blockType === "minus" ? -cappedBlack : cappedWhite;

  return {
    blockType,
    winner: whiteTime < blackTime ? "white" : blackTime < whiteTime ? "black" : null,
    cappedWhite,
    cappedBlack,
    whiteDelta,
    blackDelta,
    whiteAfter: storedMatch.clocks.white + whiteDelta,
    blackAfter: storedMatch.clocks.black + blackDelta,
  };
}

function createCubePlayerState() {
  return {
    running: false,
    startedAt: null,
    elapsedMs: 0,
  };
}

function createCubeHoldIntent() {
  return {
    armed: false,
    ready: false,
    startedAt: 0,
    pointerId: null,
    timeoutId: null,
  };
}

function normalizeCubePlayerState(playerState) {
  return {
    running: Boolean(playerState?.running),
    startedAt: typeof playerState?.startedAt === "number" ? playerState.startedAt : null,
    elapsedMs: typeof playerState?.elapsedMs === "number" ? playerState.elapsedMs : 0,
  };
}

function isAnyCubeTimerRunning(storedMatch) {
  return storedMatch.cube.playerState.white.running || storedMatch.cube.playerState.black.running;
}

function getCubePlayerElapsed(storedMatch, color) {
  const playerState = storedMatch.cube.playerState[color];

  if (storedMatch.cube.times[color] !== null) {
    return storedMatch.cube.times[color];
  }

  if (playerState.running && playerState.startedAt) {
    return playerState.elapsedMs + (Date.now() - playerState.startedAt);
  }

  return playerState.elapsedMs;
}

function formatCubePlayerTime(storedMatch, color) {
  const elapsed = getCubePlayerElapsed(storedMatch, color);
  const playerState = storedMatch.cube.playerState[color];

  if (playerState.running) {
    return formatStopwatch(elapsed);
  }

  if (elapsed <= 0 && storedMatch.cube.times[color] === null) {
    return "--";
  }

  return formatStopwatch(elapsed);
}
