import * as bus from '../../../core/bus.js';
import * as scene from '../../../core/scene.js';
import * as store from '../../../core/store.js';
import * as net from '../../../core/net.js';
import * as audio from '../../../core/audio.js';
import { t } from '../../../core/i18n.js';
import { ChessBoard } from '../canvas/board.js';
import * as engine from '../logic/engine.js';
import { ChessClock, parseTimeControl } from '../logic/clock.js';
import * as juice from '../../../core/juice.js';
import { getOpeningName } from '../logic/openings.js';
import { getMaterialAdvantage, formatAdvantage } from '../logic/material.js';
import * as emoteSystem from '../components/emotes.js';
import * as mp from '../../../core/multiplayer.js';
import * as modal from '../../../core/modal.js';
import { emoji } from '../../../core/theme.js';
import { STOCKFISH_URL } from '../../../core/config.js';
import * as matchLive from '../../../core/match-live.js';

let board, clock, gameState;
let liveSession = null;   // handle for tearing the poll/heartbeat down on exit

export function mountGame(el, params) {
  const { mode = 'bot', botId = 'amina', timeControl = 'rapid_10_0', matchId, color } = params;
  const tc = parseTimeControl(timeControl);

  scene.enterGameMode();

  // Colour has exactly one source of truth: gameState.playerColor.
  //
  // There used to be a local `playerColor` alongside it, seeded with
  // `color || 'w'`. When a caller supplied no colour BOTH players became White,
  // and because the poll only ingests the opponent's move while it is not your
  // turn, two Whites never saw each other's moves — each played a private board.
  // That is the "playing against yourself" report.
  //
  // In a live game the colour is only trusted once the server has confirmed it.
  // Until then the board is inert: a wrong guess is worse than a short wait.
  const isLive = mode === 'live';
  const startingColor = (color === 'w' || color === 'b') ? color : 'w';

  gameState = {
    mode, botId, matchId, timeControl,
    playerColor: startingColor,
    colorConfirmed: !isLive || color === 'w' || color === 'b',
    isPlayerTurn: startingColor === 'w',
    gameOver: false, moveCount: 0,
    capturedByPlayer: [], capturedByOpponent: [],
    moveHistory: [], botThinking: false,
    tournamentId: params.tournamentId || null,
    tournamentRound: params.tournamentRound || null,
    opponentRating: params.opponentRating || null
  };

  engine.create();
  clock = new ChessClock(tc.time, tc.increment);

  // Recovering after a refresh or a re-entry. The server is asked for the whole
  // truth — colour, position and clocks — before the board becomes interactive.
  // This path used to be reached with no colour at all, which meant both
  // reconnecting players became White.
  if (params.recovered && matchId) {
    gameState._recovering = true;
    gameState.colorConfirmed = false;
    net.post('game.php', { action: 'get', match_id: matchId }).then(data => {
      if (!gameState || gameState.matchId !== matchId) return;
      if (!data || data.error) { gameState._recovering = false; return; }

      const myId = store.get('auth.userId');
      const myColor = (data.my_color === 'w' || data.my_color === 'b')
        ? data.my_color
        : (data.white_player_id && myId && data.white_player_id === myId ? 'w' : 'b');

      // The game may have finished while we were away.
      if (data.status === 'completed') {
        gameState._recovering = false;
        gameState.playerColor = myColor;
        gameState.colorConfirmed = true;
        endGame(outcomeFromServerResult(data, myColor), endReasonFromServer(data),
                { serverAlreadyCompleted: true });
        return;
      }

      applyPlayerColor(myColor);
      applyServerPosition(data);
      gameState._recovering = false;
    }).catch(() => { if (gameState) gameState._recovering = false; });
  }

  el.innerHTML = `
    <div class="chess-layout" style="display:flex;flex-direction:column;height:100%;background:var(--bg-panel);justify-content:center;">
      <!-- Opponent Bar -->
      <div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:8px 14px;background:var(--bg-surface);">
        <div style="display:flex;align-items:center;gap:10px;">
          <div id="opponent-avatar" style="width:36px;height:36px;border-radius:50%;background:var(--bg-elevated);display:flex;align-items:center;justify-content:center;overflow:hidden;border:2px solid ${mode === 'bot' ? 'var(--text-muted)' : 'var(--blue)'};">
            ${mode === 'bot' ? `<img src="${STOCKFISH_URL}/portraits/${botId || 'amina'}.png" style="width:100%;height:100%;object-fit:cover;" onerror="this.style.display='none';this.parentNode.innerHTML='${emoji('robot', '🤖', 16)}'">` : `<span style="font-size:16px;">${emoji('person', '👤', 16)}</span>`}
          </div>
          <div>
            <div style="font-size:13px;font-weight:600;color:var(--text-primary);" id="opponent-name">${mode === 'bot' ? (botId || t('game.bot')) : t('game.loading_opponent')}</div>
            <div style="display:flex;gap:6px;align-items:center;">
              <div id="opponent-level" style="font-size:10px;color:var(--text-muted);">${mode === 'bot' ? t('game.bot') : ''}</div>
              <div id="opponent-captured" style="font-size:11px;color:var(--text-secondary);letter-spacing:1px;"></div>
            </div>
          </div>
        </div>
        <div id="clock-opponent" class="chess-clock">${clock.format(tc.time)}</div>
      </div>

      <!-- Board -->
      <div id="board-container" style="flex:0 0 auto;display:flex;align-items:center;justify-content:center;padding:4px 6px;position:relative;">
        <div id="bot-thinking" style="display:none;position:absolute;top:8px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:var(--gold);padding:4px 12px;border-radius:12px;font-size:12px;font-weight:600;z-index:10;">
          ${t('game.thinking')} <span class="pulse">${emoji('thinking_dots', '●●●', 12)}</span>
        </div>
        <div id="promo-dialog" style="display:none;position:absolute;z-index:20;background:var(--bg-elevated);border-radius:8px;padding:8px;box-shadow:0 8px 32px rgba(0,0,0,0.8);border:1px solid rgba(255,255,255,0.1);"></div>
      </div>

      <!-- Player Bar -->
      <div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:8px 14px;background:var(--bg-panel);">
        <div style="display:flex;align-items:center;gap:10px;">
          <div style="width:36px;height:36px;border-radius:50%;background:var(--bg-hover);display:flex;align-items:center;justify-content:center;overflow:hidden;border:2px solid var(--gold, #E4AC38);">
            ${store.get('player.avatar_url') ? `<img src="${store.get('player.avatar_url')}" style="width:100%;height:100%;object-fit:cover;">` : `<span style="font-size:16px;">${emoji('person', '👤', 16)}</span>`}
          </div>
          <div>
            <div style="font-size:13px;font-weight:600;color:var(--text-primary);">${store.get('player.display_name') || store.get('player.username') || 'You'}</div>
            <div style="display:flex;gap:6px;align-items:center;">
              <span style="font-size:10px;color:var(--text-muted);">Lv.${store.get('player.level') || 1}</span>
              <div id="player-captured" style="font-size:11px;color:var(--text-secondary);letter-spacing:1px;"></div>
            </div>
          </div>
          <button id="emote-inline-toggle" style="width:32px;height:32px;border-radius:50%;background:var(--bg-card);border:1px solid rgba(255,255,255,0.08);color:var(--text-primary);font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;margin-inline-start:4px;">${emoji('speech_bubble', '💬', 14)}</button>
        </div>
        <div id="clock-player" class="chess-clock active">${clock.format(tc.time)}</div>
      </div>

      <!-- Opening Name + Material -->
      <div style="display:flex;justify-content:space-between;align-items:center;padding:2px 14px;">
        <div id="opening-name" style="font-size:11px;color:var(--text-muted);font-style:italic;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%;"></div>
        <div id="material-diff" style="font-size:12px;font-weight:700;font-family:'SF Mono',monospace;color:var(--gold);"></div>
      </div>
      <!-- Move List -->
      <div id="move-list" style="max-height:40px;overflow-x:auto;white-space:nowrap;padding:4px 14px;font-family:'SF Mono',monospace;font-size:12px;color:var(--text-secondary);display:flex;gap:4px;align-items:center;">
        <span style="color:var(--text-dim);">1.</span>
      </div>

      <!-- Controls -->
      <div style="display:flex;gap:8px;padding:10px 14px;padding-bottom:max(10px, env(safe-area-inset-bottom, 0px));">
        <button class="ctrl-btn" id="btn-resign">${emoji('flag', '⚐', 12)} ${t('game.resign')}</button>
        <button class="ctrl-btn" id="btn-draw">½ ${t('game.draw')}</button>
        <button class="ctrl-btn" id="btn-flip">⟲ ${t('game.flip')}</button>
      </div>
    </div>
    <style>
      .chess-layout { gap: 0; }
      .chess-clock { font-size:18px;font-weight:700;font-family:'SF Mono',Inter,monospace;background:var(--bg-card);padding:6px 14px;border-radius:8px;color:var(--text-secondary);min-width:64px;text-align:center;border:1px solid var(--border); }
      .chess-clock.active { color:var(--text-primary);border-color:var(--border-hover); }
      .chess-clock.low-time { color:var(--error)!important;animation:clockPulse 1s infinite;border-color:rgba(239,68,68,0.3); }
      .ctrl-btn { flex:1;background:var(--bg-card);border:1px solid rgba(255,255,255,0.08);color:var(--text-light);font-size:13px;font-weight:600;padding:12px 8px;border-radius:10px;cursor:pointer;font-family:inherit;transition:background 0.15s,transform 0.1s;min-height:48px; }
      .ctrl-btn:active { background:#2a2a5a;transform:scale(0.95); }
      @keyframes clockPulse { 0%,100%{opacity:1}50%{opacity:0.5} }
    </style>
  `;

  const boardContainer = el.querySelector('#board-container');
  board = new ChessBoard(boardContainer, {
    flipped: gameState.playerColor === 'b',
    interactive: true,
    onMove: (from, to) => handlePlayerMove(el, from, to)
  });

  // Every one of these reads gameState.playerColor rather than capturing a
  // colour in the closure. Previously the async correction updated gameState but
  // not the captured variable, so the clocks displayed on the wrong sides and a
  // flag was awarded to the wrong player for the rest of the game.
  board.canSelect = (piece) => {
    if (gameState.gameOver || gameState.botThinking) return false;
    if (!gameState.colorConfirmed) return false;   // no guessing before the server answers
    if (!gameState.isPlayerTurn) return false;
    const pieceColor = piece === piece.toUpperCase() ? 'w' : 'b';
    return pieceColor === gameState.playerColor;
  };

  board.showLegalMoves = (square) => {
    board.highlights.legalMoves = engine.legalMoves(square);
  };

  board.setPosition(engine.fen());

  // Clock
  clock.onTick = (w, b) => {
    const playerEl = el.querySelector('#clock-player');
    const opponentEl = el.querySelector('#clock-opponent');
    if (!playerEl || !opponentEl) return;
    const [mine, theirs] = gameState.playerColor === 'w' ? [w, b] : [b, w];
    const myKey = gameState.playerColor;
    const theirKey = gameState.playerColor === 'w' ? 'b' : 'w';
    playerEl.textContent = clock.format(mine);
    opponentEl.textContent = clock.format(theirs);
    playerEl.classList.toggle('low-time', clock.isLowTime(myKey));
    opponentEl.classList.toggle('low-time', clock.isLowTime(theirKey));
  };

  clock.onFlag = (color) => endGame(color === gameState.playerColor ? 'loss' : 'win', 'timeout');

  // Controls
  el.querySelector('#btn-resign').addEventListener('click', async () => {
    if (gameState.gameOver) return;
    const confirmed = await modal.confirm(t('game.leave_confirm'), {
      title: t('game.resign'),
      icon: '🏳️',
      confirmText: t('game.resign'),
      cancelText: t('common.cancel'),
      danger: true
    });
    if (!confirmed) return;
    audio.play('gameOver', 'game');
    if (gameState.mode === 'live' && gameState.matchId) {
      await net.post('game.php', { action: 'resign', match_id: gameState.matchId }).catch(() => {});
    }
    endGame('loss', 'resign');
  });
  el.querySelector('#btn-draw').addEventListener('click', () => {
    if (gameState.gameOver) return;
    audio.play('click');
    if (gameState.mode === 'bot') {
      if (Math.random() < 0.5) {
        endGame('draw', 'agreement');
      } else {
        const msg = document.createElement('div');
        msg.textContent = t('game.draw_declined');
        msg.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.85);color:var(--red-light);padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;z-index:30;pointer-events:none;';
        el.querySelector('#board-container').appendChild(msg);
        setTimeout(() => msg.remove(), 2000);
      }
    } else if (gameState.mode === 'live' && gameState.matchId) {
      const userId = store.get('auth.userId');
      const drawT = Date.now();
      mpLogFront('draw_offer_sent', { to: 'opponent', draw_offer_t: drawT });
      net.post('game.php', {
        action: 'move',
        match_id: gameState.matchId,
        game_state: JSON.stringify({ draw_offer: userId, draw_offer_t: drawT })
      });
      const msg = document.createElement('div');
      msg.textContent = t('game.draw') + '...';
      msg.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.85);color:var(--gold);padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;z-index:30;pointer-events:none;';
      el.querySelector('#board-container').appendChild(msg);
      setTimeout(() => msg.remove(), 2500);
    }
  });
  el.querySelector('#btn-flip').addEventListener('click', () => { audio.play('click'); board.flip(); });

  // Create a real match record for bot games (so they appear in history)
  if (mode === 'bot' && !matchId) {
    net.post('game.php', {
      action: 'start',
      game_key: 'chess',
      mode: 'bot',
      time_control: timeControl,
      bot_id: botId
    }).then(data => {
      if (data && data.id) gameState.matchId = data.id;
    }).catch(() => {});
  }

  // Start bot if playing as black
  if (gameState.playerColor === 'b' && mode === 'bot') {
    clock.start('w');
    requestBotMove(el);
  }

  // Live mode: start match session (enables reconnection on refresh)
  if (mode === 'live' && matchId) {
    liveSession = matchLive.start(matchId, 'chess', {
      onMove: (data) => {
        // match-session polls every 2s — process all game events here
        handleLivePollData(el, data);
      },
      onGameEnd: (data) => {
        if (!gameState || gameState.gameOver) return;
        if (data && data.result) {
          endGame(outcomeFromServerResult(data, gameState.playerColor),
                  endReasonFromServer(data),
                  { serverAlreadyCompleted: true });
        }
        // No result on the row means the match is not actually finished —
        // do not invent a loss for this player. The old code did, which turned
        // a hiccup in the poll into a recorded defeat.
      }
    });
    if (gameState.playerColor === 'b') {
      gameState.isPlayerTurn = false;
      clock.start('w');
    }

    // Confirm colour against the match row. game.php now returns `my_color`
    // computed server-side, so no client has to work out which side it is from
    // an id comparison that silently defaults to Black when the id is missing.
    if (matchId) {
      confirmColorFromServer(el, matchId, params.opponentId);
    } else if (params.opponentId) {
      fetchAndRenderOpponent(el, params.opponentId);
    }
  }

  // Mount emote panel right after the player bar (second .chess-bar)
  const playerBar = el.querySelectorAll('.chess-bar')[1];
  const emoteContainer = el.querySelector('.chess-layout');
  emoteSystem.create(emoteContainer, (emote) => {
    audio.play('notification');
    emoteSystem.showReceived(emoteContainer, emote.emoji, playerBar);
    if (gameState.mode === 'live' && matchId) {
      mp.sendEmote(matchId, 'chess', emote.key);
    }
  });
  // Move emote panel to sit right after player bar
  const emotePanel = emoteContainer.querySelector('.emote-bar');
  if (emotePanel && playerBar && playerBar.nextSibling) {
    emoteContainer.insertBefore(emotePanel, playerBar.nextSibling);
  }

  bus.emit('game:started', { gameKey: 'chess', matchId, opponent: botId, mode });
}

function handlePlayerMove(el, from, to) {
  const moves = engine.legalMoves(from);
  const legalMove = moves.find(m => m.to === to);
  if (!legalMove) return;

  // Check promotion
  const piece = engine.get().get(from);
  if (piece && piece.type === 'p') {
    const rank = to[1];
    if ((piece.color === 'w' && rank === '8') || (piece.color === 'b' && rank === '1')) {
      showPromotionDialog(el, from, to, piece.color);
      return;
    }
  }

  executeMove(el, from, to);
}

function showPromotionDialog(el, from, to, color) {
  const dialog = el.querySelector('#promo-dialog');
  const pieces = ['q', 'r', 'b', 'n'];
  const names = { q: '♛', r: '♜', b: '♝', n: '♞' };
  if (color === 'w') { names.q = '♕'; names.r = '♖'; names.b = '♗'; names.n = '♘'; }

  dialog.style.display = 'flex';
  dialog.style.gap = '8px';
  dialog.innerHTML = pieces.map(p => `
    <div data-piece="${p}" style="width:44px;height:44px;display:flex;align-items:center;justify-content:center;font-size:28px;cursor:pointer;border-radius:6px;background:#2a2a5a;border:1px solid rgba(255,255,255,0.1);transition:background 0.15s;"
         onmouseenter="this.style.background='#3a3a7a'"
         onmouseleave="this.style.background='#2a2a5a'">
      ${names[p]}
    </div>
  `).join('');

  dialog.querySelectorAll('[data-piece]').forEach(btn => {
    btn.addEventListener('click', () => {
      dialog.style.display = 'none';
      executeMove(el, from, to, btn.dataset.piece);
    });
  });
}

function executeMove(el, from, to, promotion) {
  const m = engine.move(from, to, promotion || undefined);
  if (!m) return;

  gameState.moveCount++;
  // Animate piece sliding from → to, THEN update board state
  board.animateMove(from, to, () => {
    board.setPosition(engine.fen());
    board.setLastMove(from, to);
  });

  // Sound + Juice
  if (m.san.includes('#')) {
    audio.play('gameOver', 'game');
    juice.shake(el, 6, 400);
    juice.flash('#fff', 200);
    juice.hapticHeavy();
  } else if (m.san.includes('+')) {
    audio.play('check', 'game');
    juice.shake(el, 3, 200);
    juice.hapticMedium();
  } else if (m.captured) {
    audio.play('capture', 'game');
    juice.shake(el, 2, 150);
    juice.hapticLight();
    // Particle burst at capture square
    const captureXY = board.squareToXY(to);
    const boardRect = board.canvas.getBoundingClientRect();
    juice.burst(boardRect.left + captureXY.x + board.squareSize/2, boardRect.top + captureXY.y + board.squareSize/2,
      { count: 8, colors: ['#EF4444', '#F97316', '#FBBF24'], size: 6, spread: 60, duration: 500 });
  } else if (m.san === 'O-O' || m.san === 'O-O-O') {
    audio.play('castle', 'game');
    juice.hapticLight();
  } else {
    audio.play('move', 'game');
  }

  // Captured pieces
  if (m.captured) {
    if (m.color === gameState.playerColor) {
      gameState.capturedByPlayer.push(m.captured);
    } else {
      gameState.capturedByOpponent.push(m.captured);
    }
    updateCapturedDisplay(el);
  }

  // Check indicator
  if (engine.isCheck()) {
    board.setCheck(findKing(engine.turn()));
  } else {
    board.highlights.check = null;
    board.draw();
  }

  // Move list
  updateMoveList(el, m);

  // Clock
  if (gameState.moveCount === 1) {
    clock.start(engine.turn());
  } else {
    clock.switch();
  }

  gameState.isPlayerTurn = false;

  if (engine.isGameOver()) {
    // In live mode, send the final move BEFORE ending the game
    // so the opponent receives the checkmate/stalemate position
    if (gameState.mode === 'live') {
      sendLiveMove(el);
    }
    endGame(engine.getResult(gameState.playerColor), getEndReason());
    return;
  }

  if (gameState.mode === 'bot') {
    requestBotMove(el);
  } else if (gameState.mode === 'live') {
    // Send move to server, then poll for opponent's response
    sendLiveMove(el);
  }
}

async function requestBotMove(el, attempt = 0) {
  if (!gameState || gameState.gameOver) return;
  gameState.botThinking = true;
  const thinkingEl = el.querySelector('#bot-thinking');
  if (thinkingEl) thinkingEl.style.display = 'block';

  try {
    const data = await net.post('bots.php', {
      action: 'move',
      fen: engine.fen(),
      bot_id: gameState.botId
    });

    if (!gameState) return;
    if (thinkingEl) thinkingEl.style.display = 'none';
    gameState.botThinking = false;

    // An engine that answers but cannot produce a move leaves the board frozen
    // on "thinking…" forever unless we say something.
    if (!data.best_move && !gameState.gameOver) {
      botUnavailable(el);
      return;
    }

    if (data.best_move && !gameState.gameOver) {
      const from = data.best_move.substring(0, 2);
      const to = data.best_move.substring(2, 4);
      const promo = data.best_move.length > 4 ? data.best_move[4] : undefined;

      const m = engine.move(from, to, promo);
      if (m) {
        // Animate bot's piece sliding smoothly
        board.animateMove(from, to, () => {
          board.setPosition(engine.fen());
          board.setLastMove(from, to);
        });

        if (m.san.includes('#')) audio.play('gameOver', 'game');
        else if (m.san.includes('+')) audio.play('check', 'game');
        else if (m.captured) audio.play('capture', 'game');
        else if (m.san === 'O-O' || m.san === 'O-O-O') audio.play('castle', 'game');
        else audio.play('move', 'game');

        if (m.captured) {
          gameState.capturedByOpponent.push(m.captured);
          updateCapturedDisplay(el);
        }

        if (engine.isCheck()) {
          board.setCheck(findKing(engine.turn()));
        } else {
          board.highlights.check = null;
          board.draw();
        }

        updateMoveList(el, m);
        clock.switch();
        gameState.isPlayerTurn = true;
        gameState.moveCount++;

        if (engine.isGameOver()) {
          endGame(engine.getResult(gameState.playerColor), getEndReason());
        }
      }
    }
  } catch (e) {
    if (!gameState) return;
    if (thinkingEl) thinkingEl.style.display = 'none';
    gameState.botThinking = false;

    // This used to retry forever, so an engine outage left the player staring at
    // "thinking…" with no way forward. Back off, then give up gracefully.
    if (attempt < 4) {
      const delay = 1000 * Math.pow(2, attempt);
      setTimeout(() => requestBotMove(el, attempt + 1), delay);
    } else {
      botUnavailable(el);
    }
  }
}

/** The engine is not answering. Tell the player and let them leave. */
function botUnavailable(el) {
  if (!gameState || gameState.gameOver) return;
  gameState.botThinking = false;
  const thinkingEl = el.querySelector('#bot-thinking');
  if (thinkingEl) thinkingEl.style.display = 'none';
  mpLogFront('bot_unavailable', { botId: gameState.botId });
  modal.confirm(t('game.bot_unavailable') || 'The computer opponent is not responding. End this game?', {
    title: t('game.bot') || 'Bot',
    icon: '🤖',
    confirmText: t('common.confirm') || 'OK',
    cancelText: t('common.cancel') || 'Cancel'
  }).then(ok => {
    if (ok) endGame('draw', 'aborted');
  });
}

function updateCapturedDisplay(el) {
  const pieceSymbols = { p: '♟', n: '♞', b: '♝', r: '♜', q: '♛', k: '♚' };
  const pieceOrder = ['q', 'r', 'b', 'n', 'p'];

  const sortCaptures = (arr) => arr.sort((a, b) => pieceOrder.indexOf(a) - pieceOrder.indexOf(b));

  const playerCaptures = sortCaptures([...gameState.capturedByPlayer]);
  const opponentCaptures = sortCaptures([...gameState.capturedByOpponent]);

  el.querySelector('#player-captured').textContent = playerCaptures.map(p => pieceSymbols[p] || p).join('');
  el.querySelector('#opponent-captured').textContent = opponentCaptures.map(p => pieceSymbols[p] || p).join('');
}

function updateMoveList(el, m) {
  gameState.moveHistory.push(m);
  const moveList = el.querySelector('#move-list');
  const history = gameState.moveHistory;

  let html = '';
  for (let i = 0; i < history.length; i += 2) {
    const moveNum = Math.floor(i / 2) + 1;
    const white = history[i]?.san || '';
    const black = history[i + 1]?.san || '';
    html += `<span style="color:var(--text-dim);">${moveNum}.</span>`;
    html += `<span style="color:var(--text-primary);font-weight:500;">${white}</span>`;
    if (black) html += `<span style="color:var(--text-light);">${black}</span>`;
  }
  moveList.innerHTML = html;
  moveList.scrollLeft = moveList.scrollWidth;

  // Update opening name
  const openingEl = el.querySelector('#opening-name');
  if (openingEl && history.length <= 12) {
    const name = getOpeningName(history);
    if (name) openingEl.textContent = name;
  }

  // Update material difference
  const matEl = el.querySelector('#material-diff');
  if (matEl) {
    const adv = getMaterialAdvantage(engine.fen(), gameState.playerColor);
    matEl.textContent = formatAdvantage(adv);
    matEl.style.color = adv > 0 ? 'var(--success)' : adv < 0 ? 'var(--error)' : 'var(--text-muted)';
  }
}

function findKing(color) {
  const boardState = engine.get().board();
  for (let r = 0; r < 8; r++) {
    for (let c = 0; c < 8; c++) {
      const p = boardState[r][c];
      if (p && p.type === 'k' && p.color === color) {
        return String.fromCharCode(97 + c) + String(8 - r);
      }
    }
  }
  return null;
}

function getEndReason() {
  if (engine.isCheckmate()) return 'checkmate';
  if (engine.isStalemate()) return 'stalemate';
  if (engine.isThreefoldRepetition()) return 'threefold';
  if (engine.isInsufficientMaterial()) return 'insufficient';
  return 'draw';
}

// ===== LIVE MULTIPLAYER =====
let lastKnownMoveCount = 0;

function mpLogFront(event, data) {
  try {
    const logs = JSON.parse(sessionStorage.getItem('mp_log') || '[]');
    logs.push({ t: Date.now(), e: event, ...data });
    if (logs.length > 100) logs.splice(0, logs.length - 100);
    sessionStorage.setItem('mp_log', JSON.stringify(logs));
  } catch(e) {}
}

/**
 * 'win' | 'loss' | 'draw' from a settled server result.
 *
 * The server writes metadata.winner ('white' | 'black' | null) alongside the
 * result enum, so the outcome no longer depends on string-matching a result
 * vocabulary that the two sides disagreed about.
 */
function outcomeFromServerResult(data, myColor) {
  const meta = data && data.metadata
    ? (typeof data.metadata === 'string' ? safeParse(data.metadata) : data.metadata)
    : null;

  const winner = meta && meta.winner;
  if (winner === 'white') return myColor === 'w' ? 'win' : 'loss';
  if (winner === 'black') return myColor === 'b' ? 'win' : 'loss';
  if (winner === null && meta) return 'draw';

  // Older rows have no metadata.winner — fall back to the result enum.
  const r = data && data.result;
  if (r === 'white_wins') return myColor === 'w' ? 'win' : 'loss';
  if (r === 'black_wins') return myColor === 'b' ? 'win' : 'loss';
  return 'draw';
}

function endReasonFromServer(data) {
  const meta = data && data.metadata
    ? (typeof data.metadata === 'string' ? safeParse(data.metadata) : data.metadata)
    : null;
  return (meta && meta.end_reason) || 'unknown';
}

function safeParse(s) {
  try { return JSON.parse(s); } catch (e) { return null; }
}

/**
 * Ask the server which side we are and apply the answer.
 *
 * Retries, because being wrong about colour is the single worst failure this
 * screen has: both players end up as White and each plays a private board.
 * The board stays inert until this succeeds.
 */
async function confirmColorFromServer(el, matchId, opponentIdHint, attempt = 0) {
  try {
    const data = await net.post('game.php', { action: 'get', match_id: matchId });
    if (!data || data.error) throw new Error(data && data.error ? data.error : 'no match data');
    if (!gameState || gameState.matchId !== matchId) return;   // scene changed under us

    const myId = store.get('auth.userId');
    // Prefer the server's own answer; fall back to comparing ids only if an
    // older server build is still deployed.
    const serverColor = (data.my_color === 'w' || data.my_color === 'b')
      ? data.my_color
      : (data.white_player_id && myId && data.white_player_id === myId ? 'w' : 'b');

    applyPlayerColor(serverColor);

    const oppId = data.opponent_id
      || (data.white_player_id === myId ? data.black_player_id : data.white_player_id)
      || opponentIdHint;
    if (oppId) fetchAndRenderOpponent(el, oppId);

    // Adopt the server's position if it is ahead of ours (rejoining a game).
    if (data.current_fen && (data.move_count || 0) > gameState.moveCount) {
      applyServerPosition(data);
    }
  } catch (e) {
    if (attempt < 6 && gameState && gameState.matchId === matchId) {
      const delay = Math.min(4000, 400 * Math.pow(2, attempt));
      setTimeout(() => confirmColorFromServer(el, matchId, opponentIdHint, attempt + 1), delay);
    } else {
      mpLogFront('color_confirm_failed', { matchId, error: e.message });
    }
  }
}

/** Apply a confirmed colour everywhere it matters, in one place. */
function applyPlayerColor(colour) {
  if (!gameState) return;
  const changed = gameState.playerColor !== colour;
  gameState.playerColor = colour;
  gameState.colorConfirmed = true;
  gameState.isPlayerTurn = engine.turn() === colour;

  if (board) {
    board.flipped = colour === 'b';
    board.draw();
  }
  if (changed) mpLogFront('color_corrected', { to: colour });
}

/** Load a server position into the local engine, board and clocks. */
function applyServerPosition(data) {
  if (!data.current_fen) return;
  engine.load(data.current_fen);
  if (board) {
    board.setPosition(data.current_fen);
    board.draw();
  }
  gameState.moveCount = data.move_count || 0;
  lastKnownMoveCount = gameState.moveCount;
  gameState.isPlayerTurn = engine.turn() === gameState.playerColor;

  if (clock) {
    if (data.white_time_remaining_ms != null) clock.white = data.white_time_remaining_ms;
    if (data.black_time_remaining_ms != null) clock.black = data.black_time_remaining_ms;
    clock.start(engine.turn());
  }
  if (engine.isCheck() && board) board.setCheck(findKing(engine.turn()));
}

async function sendLiveMove(el) {
  if (!gameState.matchId) return;
  lastKnownMoveCount = gameState.moveCount;
  const lastMove = gameState.moveHistory[gameState.moveHistory.length - 1];
  mpLogFront('move_sent', { mc: gameState.moveCount, san: lastMove?.san });
  try {
    await net.post('game.php', {
      action: 'move',
      match_id: gameState.matchId,
      fen: engine.fen(),
      move: JSON.stringify(gameState.moveHistory),
      move_count: gameState.moveCount,
      white_time_remaining_ms: clock.white,
      black_time_remaining_ms: clock.black,
      game_state: JSON.stringify({
        last_move: lastMove ? { from: lastMove.from, to: lastMove.to, san: lastMove.san, promotion: lastMove.promotion || null } : null
      })
    });
  } catch (e) {
    mpLogFront('move_send_error', { error: e.message });
  }
}

function handleLivePollData(el, data) {
  if (!data || data.error) return;
  if (gameState.gameOver) return;
  if (gameState._recovering) return;

  mp.updateConnectionStatus(true);

  // Emotes — always check regardless of whose turn
  const myId = store.get('auth.userId');
  const emoteData = mp.checkForEmote(data.game_state, myId);
  if (emoteData) {
    const boardContainer = el.querySelector('#board-container');
    const oppBar = el.querySelector('.chess-bar');
    emoteSystem.showReceived(boardContainer, emoteSystem.getEmojiForKey(emoteData.key), oppBar);
    audio.play('notification');
  }

  // Draw offer/response — always check
  checkDrawOffer(el, data.game_state, myId);
  checkDrawResponse(el, data.game_state, myId);

  // Multiplayer event logging (stored in sessionStorage for debugging)
  mpLogFront('poll', { mc: data.move_count, gs_keys: data.game_state ? Object.keys(typeof data.game_state === 'string' ? JSON.parse(data.game_state) : data.game_state) : [] });

  // New move arrived — only process when waiting for opponent
  if (!gameState.isPlayerTurn && data.move_count > lastKnownMoveCount) {
    lastKnownMoveCount = data.move_count;
    const newFen = data.current_fen;
    if (newFen && newFen !== engine.fen()) {
      const oldFen = engine.fen();

      // Get move info from game_state (reliable) or fall back to FEN diff
      const gs = data.game_state ? (typeof data.game_state === 'string' ? JSON.parse(data.game_state) : data.game_state) : {};
      const serverMove = gs.last_move;
      let moveFrom = serverMove?.from;
      let moveTo = serverMove?.to;
      let moveSan = serverMove?.san;

      // Fall back to FEN diff if game_state doesn't have the move
      if (!moveFrom || !moveTo) {
        const detected = detectLastMoveSquares(oldFen, newFen);
        if (detected) { moveFrom = detected.from; moveTo = detected.to; }
      }
      if (!moveSan) {
        moveSan = detectLastMoveSan(data, oldFen, newFen);
      }

      engine.load(newFen);

      // Animate the opponent's piece
      if (moveFrom && moveTo) {
        board.animateMove(moveFrom, moveTo, () => {
          board.setPosition(newFen);
          board.setLastMove(moveFrom, moveTo);
        });
      } else {
        board.setPosition(newFen);
      }

      // Play appropriate sound + juice
      if (moveSan && moveSan.includes('#')) {
        audio.play('gameOver', 'game');
        juice.shake(el, 6, 400);
        juice.hapticHeavy();
      } else if (moveSan && moveSan.includes('+')) {
        audio.play('check', 'game');
        juice.shake(el, 3, 200);
        juice.hapticMedium();
      } else if (isCapture(oldFen, newFen)) {
        audio.play('capture', 'game');
        juice.shake(el, 2, 150);
        juice.hapticLight();
        if (moveTo) {
          const captureXY = board.squareToXY(moveTo);
          if (captureXY) {
            const boardRect = board.canvas.getBoundingClientRect();
            juice.burst(boardRect.left + captureXY.x + board.squareSize/2, boardRect.top + captureXY.y + board.squareSize/2,
              { count: 8, colors: ['#EF4444', '#F97316', '#FBBF24'], size: 6, spread: 60, duration: 500 });
          }
        }
      } else if (moveSan && (moveSan === 'O-O' || moveSan === 'O-O-O')) {
        audio.play('castle', 'game');
        juice.hapticLight();
      } else {
        audio.play('move', 'game');
      }

      // Check highlight
      if (engine.isCheck()) {
        board.setCheck(findKing(engine.turn()));
      } else {
        board.highlights.check = null;
        board.draw();
      }

      // Sync clocks from server
      if (data.white_time_remaining_ms) clock.white = data.white_time_remaining_ms;
      if (data.black_time_remaining_ms) clock.black = data.black_time_remaining_ms;
      clock.switch();

      gameState.isPlayerTurn = true;
      gameState.moveCount = data.move_count;

      // Update move list + captured pieces from server history
      if (data.moves) {
        try {
          const moveHistory = typeof data.moves === 'string' ? JSON.parse(data.moves) : data.moves;
          if (Array.isArray(moveHistory) && moveHistory.length > gameState.moveHistory.length) {
            const newMove = moveHistory[moveHistory.length - 1];
            if (newMove) {
              gameState.moveHistory.push(newMove);
              updateMoveList(el, newMove);
              if (newMove.captured) {
                gameState.capturedByOpponent.push(newMove.captured);
                updateCapturedDisplay(el);
              }
            }
          }
        } catch (e) {}
      }

      if (engine.isGameOver()) {
        endGame(engine.getResult(gameState.playerColor), getEndReason());
      }
    }
  }

  // Game ended elsewhere: the opponent resigned, a draw was accepted, someone's
  // clock ran out, or the scheduler forfeited a no-show.
  if (data.status === 'completed' && !gameState.gameOver && data.result) {
    endGame(outcomeFromServerResult(data, gameState.playerColor),
            endReasonFromServer(data),
            { serverAlreadyCompleted: true });
  }
}

function detectLastMoveSan(data, oldFen, newFen) {
  if (data.moves) {
    try {
      const moves = typeof data.moves === 'string' ? JSON.parse(data.moves) : data.moves;
      if (Array.isArray(moves) && moves.length > 0) {
        const last = moves[moves.length - 1];
        return last?.san || null;
      }
    } catch (e) {}
  }
  return null;
}

function detectLastMoveSquares(oldFen, newFen) {
  const oldPieces = parseFenPieces(oldFen);
  const newPieces = parseFenPieces(newFen);
  let from = null, to = null;
  for (const sq of Object.keys(oldPieces)) {
    if (!newPieces[sq] && oldPieces[sq]) from = sq;
  }
  for (const sq of Object.keys(newPieces)) {
    if (!oldPieces[sq] || oldPieces[sq] !== newPieces[sq]) {
      if (newPieces[sq]) to = sq;
    }
  }
  if (from && to) return { from, to };
  return null;
}

function parseFenPieces(fen) {
  const pieces = {};
  const rows = fen.split(' ')[0].split('/');
  for (let r = 0; r < 8; r++) {
    let col = 0;
    for (const ch of rows[r]) {
      if (ch >= '1' && ch <= '8') col += parseInt(ch);
      else {
        pieces[String.fromCharCode(97 + col) + String(8 - r)] = ch;
        col++;
      }
    }
  }
  return pieces;
}

function isCapture(oldFen, newFen) {
  const oldCount = oldFen.split(' ')[0].replace(/[0-9/]/g, '').length;
  const newCount = newFen.split(' ')[0].replace(/[0-9/]/g, '').length;
  return newCount < oldCount;
}

let lastDrawOfferHandled = 0;

function checkDrawOffer(el, rawGameState, myId) {
  if (!rawGameState || gameState.gameOver) return;
  const gs = typeof rawGameState === 'string' ? JSON.parse(rawGameState) : rawGameState;
  if (!gs || !gs.draw_offer) return;
  if (gs.draw_offer === myId) {
    mpLogFront('draw_offer_skip_own', { draw_offer: gs.draw_offer });
    return;
  }
  const offerTime = gs.draw_offer_t || 0;
  if (offerTime <= lastDrawOfferHandled) return;
  lastDrawOfferHandled = offerTime;
  mpLogFront('draw_offer_received', { from: gs.draw_offer, draw_offer_t: offerTime });

  // Show accept/deny UI
  const existing = el.querySelector('#draw-offer-dialog');
  if (existing) existing.remove();

  const dialog = document.createElement('div');
  dialog.id = 'draw-offer-dialog';
  dialog.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--bg-card);border:1px solid rgba(228,172,56,0.4);border-radius:12px;padding:16px 20px;z-index:50;box-shadow:0 8px 32px rgba(0,0,0,0.7);text-align:center;min-width:220px;';
  dialog.innerHTML = `
    <div style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:12px;">${t('game.draw')}?</div>
    <div style="display:flex;gap:10px;justify-content:center;">
      <button id="draw-accept" style="flex:1;padding:10px;background:var(--success);color:#000;font-weight:700;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit;">${t('common.confirm')} ½</button>
      <button id="draw-reject" style="flex:1;padding:10px;background:var(--error);color:#fff;font-weight:700;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit;">${t('common.cancel')}</button>
    </div>
  `;

  el.querySelector('#board-container').appendChild(dialog);
  audio.play('notification');

  dialog.querySelector('#draw-accept').addEventListener('click', () => {
    dialog.remove();
    net.post('game.php', {
      action: 'draw',
      match_id: gameState.matchId
    });
    endGame('draw', 'agreement', { serverAlreadyCompleted: true });
  });

  dialog.querySelector('#draw-reject').addEventListener('click', () => {
    dialog.remove();
    net.post('game.php', {
      action: 'move',
      match_id: gameState.matchId,
      game_state: JSON.stringify({ draw_offer: null, draw_offer_t: null, draw_declined: myId, draw_declined_t: Date.now() })
    });
    const msg = document.createElement('div');
    msg.textContent = t('game.draw_declined');
    msg.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.85);color:var(--red-light);padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;z-index:30;pointer-events:none;';
    el.querySelector('#board-container').appendChild(msg);
    setTimeout(() => msg.remove(), 2000);
  });

  // Auto-dismiss after 30s
  setTimeout(() => { if (dialog.parentNode) dialog.remove(); }, 30000);
}

let lastDrawDeclineHandled = 0;

function checkDrawResponse(el, rawGameState, myId) {
  if (!rawGameState || gameState.gameOver) return;
  const gs = typeof rawGameState === 'string' ? JSON.parse(rawGameState) : rawGameState;
  if (!gs) return;

  if (gs.draw_accepted) {
    endGame('draw', 'agreement', { serverAlreadyCompleted: true });
    return;
  }

  if (gs.draw_declined && gs.draw_declined !== myId && (gs.draw_declined_t || 0) > lastDrawDeclineHandled) {
    lastDrawDeclineHandled = gs.draw_declined_t;
    const msg = document.createElement('div');
    msg.textContent = t('game.draw_declined');
    msg.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.85);color:var(--red-light);padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;z-index:30;pointer-events:none;';
    el.querySelector('#board-container').appendChild(msg);
    setTimeout(() => msg.remove(), 2000);
  }
}
// ===== END LIVE MULTIPLAYER =====

function fetchAndRenderOpponent(el, oppId) {
  mp.fetchOpponentProfile(oppId).then(opp => {
    if (opp && !opp.error) {
      const nameEl = el.querySelector('#opponent-name');
      const levelEl = el.querySelector('#opponent-level');
      const avatarEl = el.querySelector('#opponent-avatar');
      if (nameEl) nameEl.textContent = opp.display_name || opp.username || t('common.opponent');
      if (levelEl) levelEl.textContent = `Lv.${opp.level || 1}`;
      if (avatarEl && opp.avatar_url) {
        avatarEl.innerHTML = `<img src="${opp.avatar_url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">`;
      }
      if (opp.chess_rating && gameState) {
        gameState.opponentRating = opp.chess_rating;
      }
    }
  }).catch(() => {});
}

function endGame(result, reason, { serverAlreadyCompleted = false } = {}) {
  if (gameState.gameOver) return;
  mp.cleanup();
  matchLive.session.destroy();
  gameState.gameOver = true;
  clock.stop();
  board.interactive = false;

  if (result === 'win') audio.play('win', 'reward');
  else if (result === 'loss') audio.play('lose', 'game');
  else audio.play('gameOver', 'game');

  const coins = result === 'win' ? 50 : result === 'draw' ? 20 : 10;
  const xp = gameState.moveCount * 2;

  const botElos = { amina: 500, tarek: 900, nour: 1100, omar: 1300, layla: 1500, ziad: 1700, grandmaster: 2100 };
  const opponentRating = gameState.mode === 'live'
    ? (gameState.opponentRating || 1200)
    : (botElos[gameState.botId] || 1200);

  let navigated = false;
  function navigateToResult(ratingChange, ratingBefore, ratingAfter) {
    if (navigated) return;
    navigated = true;
    scene.exitGameMode();
    scene.replace('chess-result', {
      result, reason, coins, xp, ratingChange,
      ratingBefore, ratingAfter,
      moves: gameState.moveCount,
      pgn: engine.pgn(),
      mode: gameState.mode,
      botId: gameState.botId,
      playerColor: gameState.playerColor,
      timeControl: gameState.timeControl,
      capturedByPlayer: gameState.capturedByPlayer,
      capturedByOpponent: gameState.capturedByOpponent,
      moveHistory: gameState.moveHistory,
      finalFen: engine.fen(),
      tournamentId: gameState.tournamentId || null
    });
    bus.emit('game:ended', { gameKey: 'chess', result, reason, mode: gameState.mode });
    if (gameState.tournamentId) {
      bus.emit('tournament:match-ended', { tournamentId: gameState.tournamentId, result });
    }
    bus.emit('coins:earned', { amount: coins });
    bus.emit('xp:earned', { amount: xp });
  }

  // If the server already completed this match (opponent resigned/drew/abandoned),
  // skip redundant complete call — just show the result
  if (serverAlreadyCompleted) {
    const fallbackRating = result === 'win' ? 12 : result === 'draw' ? 1 : -8;
    if (gameState.tournamentId) nudgeTournament(gameState.matchId, gameState.tournamentId);
    setTimeout(() => navigateToResult(fallbackRating, null, null), 1000);
    return;
  }

  // Failsafe: always show result within 5s even if network hangs
  const failsafeTimer = setTimeout(() => {
    const fallbackRating = result === 'win' ? 12 : result === 'draw' ? 1 : -8;
    navigateToResult(fallbackRating, null, null);
  }, 5000);

  // The server derives the winner itself, from the final position, from who
  // resigned, or from whose clock expired. We report only *how* the game ended.
  // This endpoint used to reject 'win' and 'loss' outright with "Invalid result
  // value", so no decisive game was ever recorded: no rating, no coins, and the
  // match stayed in_progress forever.
  const tournamentId = gameState.tournamentId;
  const matchId = gameState.matchId;

  net.post('game.php', {
    action: 'complete',
    match_id: matchId || 'local',
    reason,
    result,                       // advisory only — the server does not trust it
    fen: engine.fen(),
    pgn: engine.pgn(),
    opponent_rating: opponentRating,
    time_control: gameState.timeControl || 'rapid_10_0'
  }).then(data => {
    clearTimeout(failsafeTimer);
    const ratingChange = (data && data.rating_change != null)
      ? data.rating_change
      : (result === 'win' ? 12 : result === 'draw' ? 1 : -8);
    if (tournamentId) nudgeTournament(matchId, tournamentId);
    setTimeout(() => navigateToResult(ratingChange, data?.rating_before, data?.rating_after), 1000);
  }).catch((e) => {
    clearTimeout(failsafeTimer);
    mpLogFront('complete_failed', { error: e && e.message });
    if (tournamentId) nudgeTournament(matchId, tournamentId);
    const fallbackRating = result === 'win' ? 12 : result === 'draw' ? 1 : -8;
    setTimeout(() => navigateToResult(fallbackRating, null, null), 1000);
  });
}

/**
 * Ask the tournament to take the settled result into account.
 *
 * The server already does this in-process when the match is finalised; this is
 * only a nudge for the case where our own complete call failed but the
 * opponent's succeeded, so the round still advances promptly.
 */
function nudgeTournament(matchId, tournamentId) {
  if (!matchId || !tournamentId) return;
  net.post('tournament-match.php', {
    action: 'report-result',
    match_id: matchId,
    tournament_id: tournamentId
  }).catch(() => {});
}

export function unmountGame() {
  // The live session owns a 2s poll, a heartbeat, a disconnect watchdog and the
  // `el3ab_active_match` recovery key. None of it was torn down here, so those
  // timers kept running after the player left, and the stale recovery key pulled
  // them back into a finished game on the next app start — with no colour, which
  // is how two players both ended up as White.
  if (liveSession) {
    try { liveSession.cleanup(); } catch (e) {}
    liveSession = null;
  }
  mp.cleanup();
  if (clock) { clock.stop(); clock = null; }
  if (board) { board.destroy?.(); board = null; }
  gameState = null;
  lastKnownMoveCount = 0;
}
