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 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 PRESETS = {
  fast: {
    label: "FAST",
    quota: 6,
    description: "6 coups par joueur et par partie.",
  },
  freeze: {
    label: "FREEZE",
    quota: 8,
    description: "8 coups par joueur et par partie.",
  },
  masters: {
    label: "MASTERS",
    quota: 10,
    description: "10 coups par joueur et par partie.",
  },
};

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

let match = readStoredMatch();
let dirty = false;

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

window.addEventListener("beforeunload", flushState);
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 initSetupPage() {
  const form = document.querySelector("#setupForm");
  const summary = document.querySelector("#setupSummary");
  const loadDemoButton = document.querySelector("#loadDemoButton");
  const resumeCard = document.querySelector("#resumeCard");

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

  const renderSummary = () => {
    const mode = getRadioValue(form, "mode") || "twice";
    const preset = getRadioValue(form, "preset") || "fast";
    const quota = PRESETS[preset].quota;
    const blockDurationMs = getDurationInputMs(form, "blockSeconds", DEFAULT_BLOCK_DURATION_MS);
    const moveLimitMs = getDurationInputMs(form, "moveSeconds", DEFAULT_MOVE_LIMIT_MS);
    const timeImpact =
      mode === "time"
        ? "Chronos cumules de 10 minutes, ajustes apres chaque phase cube avec plafond de 120 s pris en compte."
        : "Le gagnant du cube commence la partie suivante, avec double coup V2 possible.";

    summary.innerHTML = `
      <strong>${MODES[mode].label}</strong>
      <span>${PRESETS[preset].description}</span>
      <span>Temps configures : partie ${formatClock(blockDurationMs)}, coup ${formatClock(moveLimitMs)}.</span>
      <span>${timeImpact}</span>
      <span>Quota actif : ${quota} coups par joueur.</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));

  form.addEventListener("submit", (event) => {
    event.preventDefault();
    const data = new FormData(form);
    const config = {
      matchLabel: sanitizeText(data.get("matchLabel")) || "Rencontre ChessCubing",
      mode: getRadioValue(form, "mode") || "twice",
      preset: getRadioValue(form, "preset") || "fast",
      blockDurationMs: getDurationInputMs(form, "blockSeconds", DEFAULT_BLOCK_DURATION_MS),
      moveLimitMs: getDurationInputMs(form, "moveSeconds", DEFAULT_MOVE_LIMIT_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;
  }

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

  const refs = {
    title: document.querySelector("#chronoTitle"),
    subtitle: document.querySelector("#chronoSubtitle"),
    blockTimer: document.querySelector("#blockTimer"),
    moveTimer: document.querySelector("#moveTimer"),
    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 de la partie demandee par l'arbitre.");
    dirty = true;
    render();
  });

  refs.arbiterTimeoutButton?.addEventListener("click", () => {
    syncRunningState(match);
    if (match.result || match.phase !== "block") {
      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);
    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;
    render();
  }

  function handlePrimaryChronoAction() {
    syncRunningState(match);

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

    if (match.phase !== "block") {
      navigateTo("cube.html");
      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 actorName = playerName(match, color);
    const active = match.currentTurn === color;

    name.textContent = actorName;
    moves.textContent = `${match.moves[color]} / ${match.quota}`;
    clock.textContent = match.clocks
      ? `Chrono ${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);

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

    if (!match.running) {
      button.textContent = "Partie en pause";
      button.disabled = true;
      hint.textContent = active
        ? "La partie n'a pas encore demarre ou a ete mise 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") {
      persistMatch();
      navigateTo("cube.html");
      return;
    }

    refs.title.textContent = match.config.matchLabel;
    refs.subtitle.textContent = `Partie ${match.blockNumber} - ${MODES[match.config.mode].label} - ${renderModeContext(match)}`;
    refs.blockTimer.textContent = formatClock(match.blockRemainingMs);
    refs.moveTimer.textContent = formatClock(match.moveRemainingMs);
    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 = `Partie ${match.blockNumber} active`;
      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 = `Partie en cours. Joueur au trait : ${playerName(match, match.currentTurn)}.`;
    } else {
      refs.centerLabel.textContent = "Trait";
      refs.centerValue.textContent = playerName(match, match.currentTurn);
      refs.spineLabel.textContent = "Pret";
      refs.spineHeadline.textContent = `Partie ${match.blockNumber}`;
      refs.spineText.textContent =
        "Demarrez la partie, puis laissez uniquement les deux grandes zones aux joueurs. La page cube prendra automatiquement le relais.";
      refs.primaryButton.textContent = "Demarrer la partie";
      refs.arbiterStatus.textContent = `Partie prete. ${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") {
      persistMatch();
      navigateTo("cube.html");
      return;
    }

    render();
  }, 100);
}

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

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

  const refs = {
    title: document.querySelector("#cubeTitle"),
    subtitle: document.querySelector("#cubeSubtitle"),
    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"),
    replayCubeButton: document.querySelector("#replayCubeButton"),
    resetButton: document.querySelector("#cubeResetButton"),
  };

  const openModal = () => toggleModal(refs.helpModal, true);
  const closeModal = () => toggleModal(refs.helpModal, 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.whiteButton?.addEventListener("click", () => {
    handleCubeTap("white");
    dirty = true;
    render();
  });

  refs.blackButton?.addEventListener("click", () => {
    handleCubeTap("black");
    dirty = true;
    render();
  });

  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) {
      if (match.config.mode === "twice" && match.cube.times.white === match.cube.times.black) {
        replayCubePhase(match);
        dirty = true;
        render();
        return;
      }

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

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

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

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

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

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

    startCubeTimer(match, color);
  }

  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 time = match.cube.times[color];

    name.textContent = playerName(match, color);
    result.textContent = formatCubePlayerTime(match, color);
    cap.textContent = renderCubeCap(match, time);

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

    button.textContent = "Demarrer mon chrono";
    button.disabled = false;
    hint.textContent = "Chaque joueur lance son propre chrono quand il commence vraiment.";
  }

  function render() {
    refs.title.textContent = match.cube.number ? `Cube n${match.cube.number}` : "Phase cube";
    refs.subtitle.textContent = `Partie ${match.blockNumber} - ${MODES[match.config.mode].label} - ${renderModeContext(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 = "Rejouer la phase cube";
      refs.helpStatus.textContent = refs.spineText.textContent;
    } else if (match.cube.times.white !== null && match.cube.times.black !== null) {
      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 la partie suivante.";
      refs.primaryButton.textContent = "Appliquer et ouvrir la page chrono";
      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 quand il veut, 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 demarrer puis arreter son propre chrono quand il le souhaite.";
      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 tape sur sa grande zone pour demarrer son propre chrono.";
      refs.primaryButton.textContent = "En attente des joueurs";
      refs.helpStatus.textContent = refs.spineText.textContent;
    }

    renderCubeZone("black");
    renderCubeZone("white");
    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: TIME_MODE_INITIAL_CLOCK_MS,
            black: TIME_MODE_INITIAL_CLOCK_MS,
          }
        : null,
    lastMover: null,
    awaitingBlockClosure: false,
    closureReason: "",
    result: null,
    cube: {
      number: null,
      running: false,
      startedAt: null,
      elapsedMs: 0,
      times: {
        white: null,
        black: null,
      },
      playerState: {
        white: createCubePlayerState(),
        black: createCubePlayerState(),
      },
      round: 1,
      history: [],
    },
    doubleCoup: {
      eligible: false,
      step: 0,
      starter: "white",
    },
    history: [],
  };

  logEvent(
    newMatch,
    `Match cree en mode ${MODES[config.mode].label}, cadence ${PRESETS[config.preset].label}, partie ${formatClock(config.blockDurationMs)} et coup ${formatClock(config.moveLimitMs)}.`,
  );
  logEvent(newMatch, "Les Blancs commencent la partie 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);

  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.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,
      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;
  }

  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);
  storedMatch.moveRemainingMs = Math.max(0, storedMatch.moveRemainingMs - delta);

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

  if (storedMatch.blockRemainingMs === 0) {
    requestBlockClosure(
      storedMatch,
      `Le temps de partie ${formatClock(getBlockDurationMs(storedMatch))} est ecoule.`,
    );
  } else if (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
      ? "Partie 1 demarre."
      : `Partie ${storedMatch.blockNumber} relance.`,
  );
}

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

  storedMatch.running = false;
  storedMatch.lastTickAt = null;
  logEvent(storedMatch, `Partie ${storedMatch.blockNumber} mise 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.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 la partie suivante.`);

  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,
    `Partie ${storedMatch.blockNumber} prete. Le trait est conserve : ${playerName(
      storedMatch,
      storedMatch.currentTurn,
    )} reprend.`,
  );
}

function applyTimeAdjustments(storedMatch, whiteTime, blackTime) {
  if (!storedMatch.clocks) {
    return;
  }

  const cappedWhite = Math.min(whiteTime, CUBE_TIME_CAP_MS);
  const cappedBlack = Math.min(blackTime, CUBE_TIME_CAP_MS);
  const blockType = getTimeBlockType(storedMatch.blockNumber);

  if (blockType === "minus") {
    storedMatch.clocks.white -= cappedWhite;
    storedMatch.clocks.black -= cappedBlack;
    logEvent(
      storedMatch,
      `Bloc - : ${formatStopwatch(cappedWhite)} retire au chrono Blanc, ${formatStopwatch(
        cappedBlack,
      )} retire au chrono Noir.`,
    );
  } else {
    storedMatch.clocks.white += cappedBlack;
    storedMatch.clocks.black += cappedWhite;
    logEvent(
      storedMatch,
      `Bloc + : ${formatStopwatch(cappedBlack)} ajoute au chrono Blanc, ${formatStopwatch(
        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.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") {
    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 renderCubeCap(storedMatch, time) {
  if (time === null) {
    return "";
  }

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

  if (time <= CUBE_TIME_CAP_MS) {
    return "plafond non atteint";
  }

  return `pris en compte ${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();
}

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

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

function loadDemo(form, onRender) {
  setInputValue(form, "matchLabel", "Demo officielle ChessCubing");
  setRadioValue(form, "mode", "twice");
  setRadioValue(form, "preset", "freeze");
  setInputValue(form, "blockSeconds", "180");
  setInputValue(form, "moveSeconds", "20");
  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 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 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 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 createCubePlayerState() {
  return {
    running: false,
    startedAt: null,
    elapsedMs: 0,
  };
}

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