(() => {
  'use strict';

  const WS_URL = window.PP1V1_WS_URL;
  const TICKET_URL = '/api/matches/ping-pong/1v1/ticket.php';
  const STATUS_URL = '/api/matches/ping-pong/1v1/status.php';
  const LOBBY_URL = '/disciplines/ping-pong/1v1/';
  const SOUND_BASE = '/disciplines/ping-pong/sounds';
  const SOUND_LIBRARY = {
    kick: `${SOUND_BASE}/kick.mp3`,
    win: `${SOUND_BASE}/won.mp3`,
    lose: `${SOUND_BASE}/gameOver.mp3`,
  };

  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  canvas.style.touchAction = 'none';

  const el = {
    status: document.getElementById('status'),
    badge: document.getElementById('badge'),
    score: document.getElementById('score'),
    btnFind: document.getElementById('btnFind'),
    btnLeave: document.getElementById('btnLeave'),
    overlay: document.getElementById('overlay'),
    overlayBadge: document.getElementById('overlayBadge'),
    overlayStage: document.getElementById('overlayStage'),
    overlayTitle: document.getElementById('overlayTitle'),
    overlayHero: document.getElementById('overlayHero'),
    overlayHeroNumber: document.getElementById('overlayHeroNumber'),
    overlayHeroLabel: document.getElementById('overlayHeroLabel'),
    overlayProgress: document.getElementById('overlayProgress'),
    overlayProgressBar: document.getElementById('overlayProgressBar'),
    overlayText: document.getElementById('overlayText'),
    overlayGrid: document.getElementById('overlayGrid'),
    overlayButtons: document.getElementById('overlayButtons'),
    overlayHint: document.getElementById('overlayHint'),
  };

  let ws = null;
  let ticket = null;
  let userId = null;
  let isConnected = false;
  let isConnecting = false;
  let isSearching = false;
  let pendingFind = false;
  let manualClose = false;

  let matchId = null;
  let side = null;
  let lastState = null;
  let lastStateAt = 0;
  let renderState = null;
  let lastRenderAt = 0;
  let lastEndPayload = null;
  let mouseAimY = null;
  let mouseControlArmed = false;

  let keyUp = false;
  let keyDown = false;
  let move = 0;
  let lastSentTargetY = null;
  let seq = 0;

  let rewardsJobId = null;
  let pollTimer = null;
  let countdownTimer = null;
  let countdownToken = null;
  let lobbyTimer = null;
  let postMatchTimer = null;
  let rewardPollState = 'idle';
  let matchMeta = null;

  const CONTROL_HINT = 'Sterowanie: myszka albo W/S albo strzałki.';
  const REWARD_ANIMATION_MS = 6500;

  const audio = Object.fromEntries(Object.entries(SOUND_LIBRARY).map(([key, src]) => {
    const clip = new Audio(src);
    clip.preload = 'auto';
    clip.volume = key === 'kick' ? 0.3 : 0.52;
    return [key, clip];
  }));

  function setStatus(text) {
    el.status.textContent = text;
  }

  function updateBadge() {
    const ownLabel = `Twój ID: ${userId ?? '—'}`;
    const opponentLabel = `ID przeciwnika: ${matchMeta?.opponentUserId ?? '—'}`;
    el.badge.textContent = `${ownLabel} • ${opponentLabel}`;
  }

  function updateButtons() {
    el.btnFind.disabled = !!matchId || isSearching;
    el.btnLeave.textContent = isSearching && !matchId ? 'Anuluj szukanie' : 'Wyjście';
  }

  function resizeCanvas() {
    const dpr = window.devicePixelRatio || 1;
    const rect = canvas.getBoundingClientRect();
    canvas.width = Math.floor(rect.width * dpr);
    canvas.height = Math.floor(rect.height * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  function showOverlay(title, text, buttons = [], options = {}) {
    el.overlay.dataset.mode = options.mode || '';
    if (options.badge) {
      el.overlayBadge.hidden = false;
      el.overlayBadge.textContent = options.badge;
    } else {
      el.overlayBadge.hidden = true;
      el.overlayBadge.textContent = '';
    }
    if (options.stage) {
      el.overlayStage.hidden = false;
      el.overlayStage.textContent = options.stage;
    } else {
      el.overlayStage.hidden = true;
      el.overlayStage.textContent = '';
    }
    el.overlayTitle.textContent = title;
    if (options.heroNumber != null || options.heroLabel) {
      el.overlayHero.hidden = false;
      el.overlayHeroNumber.textContent = options.heroNumber ?? '';
      el.overlayHeroLabel.textContent = options.heroLabel ?? '';
    } else {
      el.overlayHero.hidden = true;
      el.overlayHeroNumber.textContent = '';
      el.overlayHeroLabel.textContent = '';
    }
    if (options.progress) {
      el.overlayProgress.hidden = false;
      el.overlayProgress.dataset.indeterminate = options.progress.indeterminate ? 'true' : 'false';
      el.overlayProgressBar.style.width = `${Math.max(0, Math.min(100, Math.round((options.progress.value ?? 0) * 100)))}%`;
    } else {
      el.overlayProgress.hidden = true;
      el.overlayProgress.dataset.indeterminate = 'false';
      el.overlayProgressBar.style.width = '0%';
    }
    el.overlayText.textContent = text;
    el.overlayGrid.innerHTML = '';
    if (Array.isArray(options.gridItems) && options.gridItems.length) {
      el.overlayGrid.hidden = false;
      for (const item of options.gridItems) {
        const card = document.createElement('div');
        card.className = `overlay-card${item.tone ? ` tone-${item.tone}` : ''}`;

        const label = document.createElement('div');
        label.className = 'overlay-card-label';
        label.textContent = item.label;

        const value = document.createElement('div');
        value.className = 'overlay-card-value';
        value.textContent = item.value;

        card.append(label, value);
        el.overlayGrid.appendChild(card);
      }
    } else {
      el.overlayGrid.hidden = true;
    }
    el.overlayButtons.innerHTML = '';
    for (const button of buttons) {
      const btn = document.createElement('button');
      btn.className = 'btn' + (button.secondary ? ' secondary' : '');
      btn.textContent = button.label;
      btn.addEventListener('click', button.onClick);
      el.overlayButtons.appendChild(btn);
    }
    el.overlayButtons.style.display = buttons.length ? 'flex' : 'none';
    el.overlayHint.textContent = options.hint || CONTROL_HINT;
    el.overlay.style.display = 'flex';
  }

  function hideOverlay() {
    el.overlay.dataset.mode = '';
    el.overlayBadge.hidden = true;
    el.overlayBadge.textContent = '';
    el.overlayStage.hidden = true;
    el.overlayStage.textContent = '';
    el.overlayHero.hidden = true;
    el.overlayHeroNumber.textContent = '';
    el.overlayHeroLabel.textContent = '';
    el.overlayProgress.hidden = true;
    el.overlayProgress.dataset.indeterminate = 'false';
    el.overlayProgressBar.style.width = '0%';
    el.overlayGrid.hidden = true;
    el.overlayGrid.innerHTML = '';
    el.overlayButtons.style.display = 'flex';
    el.overlayHint.textContent = CONTROL_HINT;
    el.overlay.style.display = 'none';
  }

  function clearCountdownTimer() {
    if (countdownTimer) {
      clearInterval(countdownTimer);
      countdownTimer = null;
    }
    countdownToken = null;
  }

  function clearLobbyTimer() {
    if (lobbyTimer) {
      clearTimeout(lobbyTimer);
      lobbyTimer = null;
    }
  }

  function clearPostMatchTimer() {
    if (postMatchTimer) {
      clearInterval(postMatchTimer);
      postMatchTimer = null;
    }
  }

  function setOverlayStage(text) {
    el.overlayStage.hidden = !text;
    el.overlayStage.textContent = text || '';
  }

  function setOverlayHero(number, label) {
    el.overlayHero.hidden = number == null && !label;
    el.overlayHeroNumber.textContent = number ?? '';
    el.overlayHeroLabel.textContent = label ?? '';
  }

  function setOverlayProgress(value, indeterminate = false) {
    el.overlayProgress.hidden = false;
    el.overlayProgress.dataset.indeterminate = indeterminate ? 'true' : 'false';
    el.overlayProgressBar.style.width = `${Math.max(0, Math.min(100, Math.round(value * 100)))}%`;
  }

  function setOverlayHint(text) {
    el.overlayHint.textContent = text || CONTROL_HINT;
  }

  function shouldKeepOverlayVisible() {
    return el.overlay.dataset.mode === 'countdown' || el.overlay.dataset.mode === 'victory' || el.overlay.dataset.mode === 'defeat';
  }

  function getSideDescriptor(currentSide) {
    if (currentSide === 'left') {
      return {
        label: 'lewa',
        color: 'niebieski',
        tone: 'blue',
        moveHint: 'Twoja paletka jest po lewej stronie stołu.',
      };
    }
    return {
      label: 'prawa',
      color: 'różowy',
      tone: 'pink',
      moveHint: 'Twoja paletka jest po prawej stronie stołu.',
    };
  }

  function buildPreMatchGrid(meta) {
    const sideInfo = getSideDescriptor(meta.side);
    return [
      { label: 'Przeciwnik', value: meta.opponentUsername || 'Łączenie…' },
      { label: 'ID przeciwnika', value: meta.opponentUserId ?? '—' },
      { label: 'Strona', value: sideInfo.label, tone: sideInfo.tone },
      { label: 'Kolor', value: sideInfo.color, tone: sideInfo.tone },
      { label: 'Sterowanie', value: 'Myszka lub W/S lub strzałki' },
      { label: 'Sety', value: `do ${meta.setsToWin ?? 3} wygranych`, tone: 'gold' },
      { label: 'Punkty', value: `do ${meta.pointsToWin ?? 11}`, tone: 'gold' },
    ];
  }

  function buildPostMatchGrid(payload, didWin) {
    const opponentUsername = didWin
      ? (payload?.loserUsername || matchMeta?.opponentUsername || 'przeciwnik')
      : (payload?.winnerUsername || matchMeta?.opponentUsername || 'przeciwnik');
    const opponentUserId = didWin
      ? (payload?.loserUserId || matchMeta?.opponentUserId || '—')
      : (payload?.winnerUserId || matchMeta?.opponentUserId || '—');
    return [
      { label: 'Przeciwnik', value: opponentUsername },
      { label: 'ID przeciwnika', value: opponentUserId },
      { label: 'Wynik setów', value: `${payload?.sets?.left ?? 0}:${payload?.sets?.right ?? 0}`, tone: 'gold' },
      { label: 'Wynik punktów', value: `${payload?.points?.left ?? 0}:${payload?.points?.right ?? 0}` },
      { label: 'Powód zakończenia', value: payload?.reason === 'sets' ? 'zwycięstwo w setach' : (payload?.reason || 'koniec meczu'), tone: didWin ? 'blue' : 'pink' },
    ];
  }

  function returnToLobby() {
    clearCountdownTimer();
    clearLobbyTimer();
    clearPostMatchTimer();
    if (pollTimer) {
      clearInterval(pollTimer);
      pollTimer = null;
    }
    localStorage.removeItem('pp1v1.matchId');
    rewardsJobId = null;
    matchMeta = null;
    matchId = null;
    side = null;
    updateBadge();
    manualClose = true;
    try {
      if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
        ws.close(1000, 'return_to_lobby');
      }
    } catch {
      // ignore
    }
    window.location.href = LOBBY_URL;
  }

  function startMatchCountdown(meta) {
    matchMeta = {
      ...matchMeta,
      ...meta,
    };
    updateBadge();

    const token = `${matchMeta.matchId || matchId || 'match'}:${matchMeta.side}:${matchMeta.warmupEndsAt}`;
    if (countdownToken === token && countdownTimer) {
      return;
    }

    clearCountdownTimer();
    countdownToken = token;

    const sideInfo = getSideDescriptor(matchMeta.side);
    showOverlay(
      'Mecz startuje za chwilę',
      `Grasz z ${matchMeta.opponentUsername || 'przeciwnikiem'}. ${sideInfo.moveHint}`,
      [],
      {
        mode: 'countdown',
        badge: 'Przygotuj się',
        stage: `Stoły gotowe • mecz do ${matchMeta.pointsToWin ?? 11} punktów i ${matchMeta.setsToWin ?? 3} setów`,
        heroNumber: '10',
        heroLabel: 'sekund do startu',
        progress: { value: 0 },
        gridItems: buildPreMatchGrid(matchMeta),
        hint: `${CONTROL_HINT} ${sideInfo.moveHint}`,
      }
    );

    const totalMs = Math.max(1000, (matchMeta.warmupEndsAt || (Date.now() + 10_000)) - Date.now());
    const updateCountdown = () => {
      const remainingMs = Math.max(0, (matchMeta?.warmupEndsAt || Date.now()) - Date.now());
      const remainingSeconds = Math.max(0, Math.ceil(remainingMs / 1000));
      setOverlayHero(String(remainingSeconds), remainingSeconds === 1 ? 'sekunda do startu' : 'sekund do startu');
      setOverlayProgress(1 - (remainingMs / totalMs));
      setOverlayStage(`Grasz kolorem ${sideInfo.color} po stronie ${sideInfo.label}.`);

      if (remainingMs <= 0) {
        clearCountdownTimer();
        hideOverlay();
        setStatus(`Mecz trwa z ${matchMeta?.opponentUsername || 'przeciwnikiem'}.`);
      }
    };

    updateCountdown();
    countdownTimer = setInterval(updateCountdown, 100);
  }

  function startPostMatchAnimation(payload) {
    const didWin = didWinLastMatch();
    const title = didWin ? 'Zwycięstwo!' : 'Porażka';
    const opponentUsername = didWin
      ? (payload?.loserUsername || matchMeta?.opponentUsername || 'przeciwnik')
      : (payload?.winnerUsername || matchMeta?.opponentUsername || 'przeciwnik');
    const heroScore = `${payload?.sets?.left ?? 0}:${payload?.sets?.right ?? 0}`;
    const stages = didWin
      ? ['Potwierdzanie wyniku', 'Przydzielanie nagrody', 'Zamykanie stołu', 'Powrót do lobby 1v1']
      : ['Zapisywanie wyniku', 'Przydzielanie nagrody pocieszenia', 'Zamykanie stołu', 'Powrót do lobby 1v1'];

    clearCountdownTimer();
    clearLobbyTimer();
    clearPostMatchTimer();
    localStorage.removeItem('pp1v1.matchId');
    rewardPollState = 'pending';

    showOverlay(
      title,
      `${didWin ? 'Pokonałeś' : 'Przegrałeś z'} ${opponentUsername}. Za chwilę wrócisz automatycznie do lobby 1v1.`,
      [],
      {
        mode: didWin ? 'victory' : 'defeat',
        badge: didWin ? 'Wygrana' : 'Porażka',
        stage: stages[0],
        heroNumber: heroScore,
        heroLabel: 'wynik setów',
        progress: { value: 0.05 },
        gridItems: buildPostMatchGrid(payload, didWin),
        hint: 'Wynik został zapisany. Za chwilę nastąpi powrót do ekranu szukania meczu.',
      }
    );

    const startedAt = Date.now();
    const tick = () => {
      const elapsed = Date.now() - startedAt;
      const progress = Math.min(1, elapsed / REWARD_ANIMATION_MS);
      const stageIndex = Math.min(stages.length - 1, Math.floor(progress * stages.length));
      const remainingSeconds = Math.max(0, Math.ceil((REWARD_ANIMATION_MS - elapsed) / 1000));

      setOverlayProgress(progress, rewardPollState === 'pending');
      setOverlayStage(`${stages[stageIndex]}${remainingSeconds ? ` • ${remainingSeconds}s` : ''}`);

      if (rewardPollState === 'done') {
        setOverlayHint('Nagrody zostały przyznane. Za chwilę wracasz do lobby 1v1.');
      } else if (rewardPollState === 'failed') {
        setOverlayHint('Rozliczenie nagród domknie się w tle. Powrót do lobby 1v1 za chwilę.');
      }

      if (elapsed >= REWARD_ANIMATION_MS) {
        clearPostMatchTimer();
        returnToLobby();
      }
    };

    tick();
    postMatchTimer = setInterval(tick, 120);
  }

  function playSound(name) {
    const source = audio[name];
    if (!source) return;
    try {
      const clip = source.cloneNode();
      clip.volume = source.volume;
      clip.play().catch(() => {});
    } catch {
      // ignore browser audio restrictions
    }
  }

  function didWinLastMatch() {
    return !!lastEndPayload && !!userId && lastEndPayload.winnerUserId === userId;
  }

  function formatMatchScore(state) {
    const setsL = Number.isFinite(state?.setsL) ? state.setsL : 0;
    const setsR = Number.isFinite(state?.setsR) ? state.setsR : 0;
    const scoreL = Number.isFinite(state?.scoreL) ? state.scoreL : 0;
    const scoreR = Number.isFinite(state?.scoreR) ? state.scoreR : 0;
    return `${setsL}:${setsR} sety • ${scoreL}:${scoreR}`;
  }

  async function fetchTicket() {
    const res = await fetch(TICKET_URL, { method: 'GET', credentials: 'include' });
    const json = await res.json().catch(() => null);
    if (!res.ok || !json?.success) {
      throw new Error(json?.error || `Ticket error ${res.status}`);
    }
    return json.ticket;
  }

  function send(msg) {
    if (!ws || ws.readyState !== WebSocket.OPEN) return;
    ws.send(JSON.stringify(msg));
  }

  async function ensureConnected() {
    if (isConnected && ws && ws.readyState === WebSocket.OPEN) return;
    if (isConnecting) return;
    await connect();
  }

  function joinQueue() {
    pendingFind = false;
    isSearching = false;
    updateButtons();
    send({ type: 'queue.join' });
    setStatus('Dołączanie do kolejki…');
  }

  function leaveQueue() {
    if (!isSearching) return;
    isSearching = false;
    pendingFind = false;
    updateButtons();
    send({ type: 'queue.leave' });
    setStatus('Wyszukiwanie anulowane.');
  }

  function computeMove() {
    if (keyUp && !keyDown) return -1;
    if (keyDown && !keyUp) return 1;
    return 0;
  }

  function getDesiredTargetY() {
    if (keyUp || keyDown) return null;
    if (!mouseControlArmed) return null;
    if (mouseAimY == null) return null;
    return clamp(mouseAimY, 0.12, 0.88);
  }

  function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
  }

  function lerp(current, target, amount) {
    return current + (target - current) * amount;
  }

  function cloneRenderState(state) {
    return {
      paddleL: { y: state.paddleL.y },
      paddleR: { y: state.paddleR.y },
      ball: {
        x: state.ball.x,
        y: state.ball.y,
        vx: state.ball.vx || 0,
        vy: state.ball.vy || 0,
      },
    };
  }

  function predictBallY(y, vy, dt, min, max) {
    let nextY = y + vy * dt;
    let nextVy = vy;
    let safety = 0;

    while ((nextY < min || nextY > max) && safety < 4) {
      if (nextY < min) {
        nextY = min + (min - nextY);
        nextVy = Math.abs(nextVy);
      } else {
        nextY = max - (nextY - max);
        nextVy = -Math.abs(nextVy);
      }
      safety += 1;
    }

    return { y: clamp(nextY, min, max), vy: nextVy };
  }

  function getRenderState(now) {
    if (!lastState) return null;
    if (!renderState) {
      renderState = cloneRenderState(lastState);
      lastRenderAt = now;
      return renderState;
    }

    const frameDt = lastRenderAt ? Math.min(0.05, Math.max(0.001, (now - lastRenderAt) / 1000)) : 0.016;
    lastRenderAt = now;

    const snapshotAge = Math.min(0.08, Math.max(0, (Date.now() - lastStateAt) / 1000));
    const targetBallX = clamp(lastState.ball.x + (lastState.ball.vx || 0) * snapshotAge, 0, 1);
    const predictedBall = predictBallY(lastState.ball.y, lastState.ball.vy || 0, snapshotAge, 0.015, 0.985);

    const paddleLerp = Math.min(1, frameDt * 16);
    const ballLerp = Math.min(1, frameDt * 20);

    renderState.paddleL.y = lerp(renderState.paddleL.y, lastState.paddleL.y, paddleLerp);
    renderState.paddleR.y = lerp(renderState.paddleR.y, lastState.paddleR.y, paddleLerp);
    renderState.ball.x = lerp(renderState.ball.x, targetBallX, ballLerp);
    renderState.ball.y = lerp(renderState.ball.y, predictedBall.y, ballLerp);
    renderState.ball.vx = lastState.ball.vx || 0;
    renderState.ball.vy = predictedBall.vy;

    return renderState;
  }

  function updateMouseAim(event) {
    const rect = canvas.getBoundingClientRect();
    if (!rect.width || !rect.height) return;
    const clientY = event.clientY ?? event.pageY;
    if (!Number.isFinite(clientY)) return;
    const y = (clientY - rect.top) / rect.height;
    mouseAimY = Math.max(0.12, Math.min(0.88, y));
    mouseControlArmed = true;
  }

  function setupInput() {
    window.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') keyUp = true;
      if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') keyDown = true;
      if (['ArrowUp', 'ArrowDown', 'w', 'W', 's', 'S'].includes(e.key)) {
        mouseControlArmed = false;
      }
      if (['ArrowUp', 'ArrowDown', 'w', 'W', 's', 'S'].includes(e.key)) e.preventDefault();
    }, { passive: false });

    window.addEventListener('keyup', (e) => {
      if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') keyUp = false;
      if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') keyDown = false;
    });

    canvas.addEventListener('pointermove', (event) => {
      updateMouseAim(event);
    });

    canvas.addEventListener('pointerenter', (event) => {
      updateMouseAim(event);
    });

    canvas.addEventListener('mousemove', (event) => {
      updateMouseAim(event);
    });

    canvas.addEventListener('pointerdown', (event) => {
      updateMouseAim(event);
    });

    canvas.addEventListener('mouseleave', () => {
      mouseAimY = null;
      mouseControlArmed = false;
    });

    canvas.addEventListener('pointerleave', () => {
      mouseAimY = null;
      mouseControlArmed = false;
    });

    setInterval(() => {
      const next = computeMove();
      const nextTargetY = getDesiredTargetY();
      const targetChanged = nextTargetY == null
        ? lastSentTargetY != null
        : lastSentTargetY == null || Math.abs(nextTargetY - lastSentTargetY) > 0.004;

      if (next === move && !targetChanged) return;
      move = next;
      lastSentTargetY = nextTargetY;
      send({ type: 'match.input', seq: ++seq, move, targetY: nextTargetY });
    }, 33);
  }

  function draw() {
    const now = performance.now();
    const w = canvas.clientWidth;
    const h = canvas.clientHeight;
    const radius = Math.min(18, w * 0.025, h * 0.04);

    ctx.clearRect(0, 0, w, h);

    ctx.save();
    clipRoundedRect(ctx, 0, 0, w, h, radius);

    ctx.fillStyle = '#0a0a0a';
    ctx.fillRect(0, 0, w, h);

    ctx.save();
    ctx.fillStyle = 'rgba(0,255,247,0.06)';
    ctx.fillRect(0, 0, w * 0.12, h);
    ctx.fillStyle = 'rgba(255,0,110,0.06)';
    ctx.fillRect(w * 0.88, 0, w * 0.12, h);
    ctx.restore();

    ctx.save();
    ctx.strokeStyle = 'rgba(0,255,247,0.25)';
    ctx.setLineDash([10, 10]);
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(w / 2, 0);
    ctx.lineTo(w / 2, h);
    ctx.stroke();
    ctx.restore();

    if (!lastState) {
      ctx.fillStyle = 'rgba(223,252,255,0.75)';
      ctx.font = '16px Lato, sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('Połącz się i kliknij "Szukaj meczu"', w / 2, h / 2);
      ctx.restore();
      requestAnimationFrame(draw);
      return;
    }

    const state = getRenderState(now);
    const paddleHalf = 0.12;
    const paddleH = (paddleHalf * 2) * h;
    const paddleW = Math.max(10, Math.floor(w * 0.012));

    const xL = 0.06 * w;
    const xR = 0.94 * w;

    const yL = (state.paddleL.y * h) - paddleH / 2;
    const yR = (state.paddleR.y * h) - paddleH / 2;

    ctx.save();
    ctx.shadowBlur = 20;
    ctx.shadowColor = '#0080ff';
    ctx.fillStyle = '#0080ff';
    ctx.fillRect(xL - paddleW / 2, yL, paddleW, paddleH);

    ctx.shadowColor = '#ff006e';
    ctx.fillStyle = '#ff006e';
    ctx.fillRect(xR - paddleW / 2, yR, paddleW, paddleH);
    ctx.restore();

    const ballR = Math.max(6, Math.floor(Math.min(w, h) * 0.015));
    const bx = state.ball.x * w;
    const by = state.ball.y * h;

    ctx.save();
    ctx.shadowBlur = 30;
    ctx.shadowColor = '#00fff7';
    ctx.fillStyle = '#00fff7';
    ctx.beginPath();
    ctx.arc(bx, by, ballR, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();

    ctx.restore();

    requestAnimationFrame(draw);
  }

  function clipRoundedRect(context, x, y, width, height, radius) {
    context.beginPath();
    context.moveTo(x + radius, y);
    context.lineTo(x + width - radius, y);
    context.quadraticCurveTo(x + width, y, x + width, y + radius);
    context.lineTo(x + width, y + height - radius);
    context.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
    context.lineTo(x + radius, y + height);
    context.quadraticCurveTo(x, y + height, x, y + height - radius);
    context.lineTo(x, y + radius);
    context.quadraticCurveTo(x, y, x + radius, y);
    context.closePath();
    context.clip();
  }

  async function pollRewards(jobId) {
    if (pollTimer) clearInterval(pollTimer);

    const tick = async () => {
      try {
        const res = await fetch(`${STATUS_URL}?jobId=${encodeURIComponent(jobId)}`, { credentials: 'include' });
        const json = await res.json().catch(() => null);
        if (!res.ok || !json?.success) return;

        const st = json.job?.status;
        if (st === 'done') {
          rewardPollState = 'done';
          clearInterval(pollTimer);
          pollTimer = null;
          return;
        }

        if (st === 'failed') {
          rewardPollState = 'failed';
          clearInterval(pollTimer);
          pollTimer = null;
        }
      } catch {
        // ignore
      }
    };

    await tick();
    pollTimer = setInterval(tick, 1200);
  }

  async function connect() {
    if (!WS_URL) {
      showOverlay('Konfiguracja', 'Brak PP1V1_WS_URL na stronie.', [
        { label: 'Wróć', onClick: () => window.location.href = '/disciplines/ping-pong/' }
      ]);
      return;
    }

    if (isConnecting) {
      return;
    }

    isConnecting = true;

    try {
      setStatus('Pobieranie ticketu…');
      ticket = await fetchTicket();

      setStatus('Łączenie z serwerem…');
      await new Promise((resolve, reject) => {
        let settled = false;
        let helloReceived = false;

        const fail = (message) => {
          if (settled) return;
          settled = true;
          isConnecting = false;
          isConnected = false;
          try {
            if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
              manualClose = true;
              ws.close();
            }
          } catch {
            // ignore
          }
          reject(new Error(message));
        };

        ws = new WebSocket(WS_URL);

        ws.addEventListener('open', () => {
          const lastMatch = localStorage.getItem('pp1v1.matchId');
          send({ type: 'hello', ticket, matchId: lastMatch || undefined });
        });

        ws.addEventListener('error', () => {
          fail('Nie udało się połączyć z serwerem gry. Adres WebSocket nie odpowiada lub reverse proxy dla /ping-pong-1v1 nie jest skonfigurowane.');
        });

        ws.addEventListener('message', (ev) => {
          let msg;
          try { msg = JSON.parse(ev.data); } catch { return; }

          if (msg.type === 'hello.ok') {
            helloReceived = true;
            settled = true;
            isConnecting = false;
            userId = msg.userId;
            isConnected = true;
            updateBadge();
            setStatus('Połączono.');
            updateButtons();
            if (!shouldKeepOverlayVisible()) {
              hideOverlay();
            }
            if (pendingFind) joinQueue();
            resolve();
            return;
          }

          if (msg.type === 'match.reconnected') {
            matchId = msg.matchId;
            side = msg.side || side;
            matchMeta = {
              ...matchMeta,
              matchId: msg.matchId,
              side: msg.side || side,
              opponentUserId: msg.opponentUserId || matchMeta?.opponentUserId,
              opponentUsername: msg.opponentUsername || matchMeta?.opponentUsername,
              warmupEndsAt: msg.warmupEndsAt || matchMeta?.warmupEndsAt,
              pointsToWin: msg.pointsToWin || matchMeta?.pointsToWin,
              setsToWin: msg.setsToWin || matchMeta?.setsToWin,
            };
            if ((msg.warmupEndsAt || 0) > Date.now()) {
              startMatchCountdown(matchMeta);
            }
            return;
          }

          if (msg.type === 'hello.error') {
            if (msg.error === 'duplicate_session') {
              fail('To konto jest już połączone z trybem 1v1 w innej karcie lub przeglądarce. Zamknij tamtą sesję i spróbuj ponownie.');
              return;
            }
            if (msg.error === 'missing_username' || msg.error === 'invalid_username') {
              fail('Nie możesz wejść do gry bez poprawnego username. Ustaw nick na koncie i zaloguj się ponownie.');
              return;
            }
            fail(msg.error || 'Autoryzacja WebSocket nie powiodła się.');
            return;
          }

          if (msg.type === 'queue.status') {
            isSearching = msg.status === 'searching';
            updateButtons();
            if (msg.status === 'searching') {
              const suffix = Number.isFinite(Number(msg.queueSize)) ? ` (${msg.queueSize} w kolejce)` : '';
              setStatus('Szukam przeciwnika…' + suffix);
            } else {
              setStatus('Gotowy do wyszukania meczu.');
            }
            return;
          }

          if (msg.type === 'error') {
            isSearching = false;
            updateButtons();
            if (msg.error === 'duplicate_session') {
              fail('To konto jest już połączone z trybem 1v1 w innej karcie lub przeglądarce. Zamknij tamtą sesję i spróbuj ponownie.');
              return;
            }
            if (msg.error === 'already_in_match') {
              fail('To konto jest już w aktywnym meczu 1v1.');
              return;
            }
            setStatus('Nie udało się dołączyć do kolejki.');
            fail(msg.error || 'Serwer odrzucił żądanie.');
            return;
          }

          if (msg.type === 'match.found') {
            matchId = msg.matchId;
            side = msg.side;
            matchMeta = {
              matchId,
              side,
              opponentUserId: msg.opponentUserId || null,
              opponentUsername: msg.opponentUsername || 'przeciwnik',
              warmupEndsAt: msg.warmupEndsAt || (Date.now() + 10_000),
              pointsToWin: msg.pointsToWin || 11,
              setsToWin: msg.setsToWin || 3,
            };
            lastSentTargetY = null;
            isSearching = false;
            renderState = null;
            lastRenderAt = 0;
            localStorage.setItem('pp1v1.matchId', matchId);
            updateButtons();
            setStatus(`Mecz znaleziony z ${matchMeta.opponentUsername}.`);
            startMatchCountdown(matchMeta);
            return;
          }

          if (msg.type === 'match.state') {
            if (lastState) {
              const scoreChanged = lastState.scoreL !== msg.state.scoreL || lastState.scoreR !== msg.state.scoreR;
              const vxFlipped = Math.sign(lastState.ball.vx || 0) !== Math.sign(msg.state.ball.vx || 0);
              const vyFlipped = Math.sign(lastState.ball.vy || 0) !== Math.sign(msg.state.ball.vy || 0);
              if (scoreChanged || vxFlipped || vyFlipped) {
                playSound('kick');
              }
            }
            lastState = msg.state;
            lastStateAt = Date.now();
            if (!renderState) {
              renderState = cloneRenderState(lastState);
              lastRenderAt = 0;
            }
            if (msg.state?.warmupEndsAt) {
              matchMeta = {
                ...matchMeta,
                matchId: msg.matchId,
                side,
                warmupEndsAt: msg.state.warmupEndsAt,
                pointsToWin: msg.state.pointsToWin,
                setsToWin: msg.state.setsToWin,
              };
              if (msg.state.isWarmup && side) {
                startMatchCountdown(matchMeta);
              } else if (!msg.state.isWarmup && el.overlay.dataset.mode === 'countdown') {
                clearCountdownTimer();
                hideOverlay();
              }
            }
            el.score.textContent = formatMatchScore(lastState);
            return;
          }

          if (msg.type === 'match.end') {
            isSearching = false;
            lastEndPayload = msg.payload ?? null;
            renderState = null;
            lastRenderAt = 0;
            matchId = null;
            setStatus('Mecz zakończony.');
            updateButtons();
            playSound(didWinLastMatch() ? 'win' : 'lose');
            startPostMatchAnimation(msg.payload);
            return;
          }

          if (msg.type === 'rewards.queued') {
            const jobId = msg.response?.jobId;
            if (jobId) {
              rewardsJobId = jobId;
              pollRewards(jobId);
            }
            return;
          }

          if (msg.type === 'rewards.error') {
            rewardPollState = 'failed';
          }
        });

        ws.addEventListener('close', async () => {
          isConnected = false;
          updateButtons();
          if (!settled && !helloReceived) {
            fail('Nie udało się zestawić połączenia WebSocket. Publiczny adres /ping-pong-1v1 zwraca 404 albo nie jest podpięty do serwera Node.');
            return;
          }
          if (manualClose) {
            manualClose = false;
            return;
          }
          setStatus('Rozłączono. Próba ponownego połączenia…');
          for (let i = 0; i < 5; i++) {
            await new Promise(r => setTimeout(r, 500 + i * 700));
            try {
              await connect();
              return;
            } catch {
              // keep trying
            }
          }
          showOverlay('Połączenie', 'Nie udało się połączyć z serwerem.', [
            { label: 'Wróć do menu', onClick: () => window.location.href = '/disciplines/ping-pong/' }
          ]);
        });
      });
    } finally {
      isConnecting = false;
    }
  }

  function wireButtons() {
    updateButtons();

    el.btnFind.addEventListener('click', async () => {
      if (matchId || isSearching) return;
      if (!isConnected || !ws || ws.readyState !== WebSocket.OPEN) {
        pendingFind = true;
        try {
          await ensureConnected();
        } catch (e) {
          pendingFind = false;
          showOverlay('Błąd', String(e.message || e), [{ label: 'OK', onClick: () => hideOverlay() }]);
        }
        return;
      }
      joinQueue();
    });

    el.btnLeave.addEventListener('click', () => {
      if (isSearching && !matchId) {
        leaveQueue();
        return;
      }
      manualClose = true;
      if (ws && ws.readyState === WebSocket.OPEN) {
        if (isSearching) send({ type: 'queue.leave' });
        ws.close(1000, 'user_left');
      }
      mouseAimY = null;
      mouseControlArmed = false;
      lastSentTargetY = null;
      localStorage.removeItem('pp1v1.matchId');
      window.location.href = '/disciplines/ping-pong/';
    });

    window.addEventListener('beforeunload', () => {
      if (isSearching) {
        send({ type: 'queue.leave' });
      }
    });
  }

  window.addEventListener('resize', resizeCanvas);
  resizeCanvas();
  setupInput();
  wireButtons();
  requestAnimationFrame(draw);

  showOverlay('Ping-Pong 1v1', 'Połącz się z serwerem i znajdź mecz. Po znalezieniu przeciwnika zagrasz mecz 3 setowy do 11 punktów.', [
    { label: 'Połącz', onClick: () => connect().catch(e => showOverlay('Błąd', String(e.message || e), [{ label: 'OK', onClick: () => hideOverlay() }])) },
    { label: 'Wróć', secondary: true, onClick: () => window.location.href = '/disciplines/ping-pong/' },
  ], {
    badge: 'Tryb online',
    stage: 'Matchmaking 1v1',
    gridItems: [
      { label: 'Sterowanie', value: 'Myszka albo W/S albo strzałki' },
      { label: 'Mecz', value: 'Do 11 punktów', tone: 'gold' },
      { label: 'Sety', value: 'Do 3 wygranych', tone: 'gold' },
      { label: 'Po meczu', value: 'Wygrywasz nagrodę całej puli!' },
    ],
  });
})();
