/**
 * Tetris Battle Royale Server
 *
 * Express + Socket.io server that handles all game logic authoritatively.
 * Manages a single global lobby with 2-8 players competing in real-time.
 *
 * @module server
 */

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');

// Import pure game logic functions
const {
  BOARD_WIDTH,
  BOARD_HEIGHT,
  GARBAGE_COLOR,
  TETROMINOS,
  TETROMINO_KEYS,
  createEmptyBoard,
  getPieceFromType,
  isValidPosition,
  getGhostY,
  checkBoardOverflow,
  addGarbageRow,
  lockPieceToBoard,
  clearRows: clearRowsFromBoard,
  generatePieceBag,
  createPieceQueue
} = require('./game-logic');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

// Serve static files
app.use(express.static(path.join(__dirname, '../public')));

/**
 * @typedef {Object} Player
 * @property {string} id - Socket.io connection ID
 * @property {string} name - Player's display name
 * @property {number} score - Current score
 * @property {number} lines - Total lines cleared
 * @property {number} level - Current level (affects drop speed)
 * @property {number[][]} board - 20x10 game board
 * @property {object|null} currentPiece - Currently falling piece
 * @property {object|null} nextPiece - Next piece to spawn
 * @property {object|null} holdPiece - Held piece (for hold mechanic)
 * @property {boolean} canHold - Whether hold is available this turn
 * @property {boolean} eliminated - Whether player has been eliminated
 * @property {boolean} ready - Whether player is ready to start
 * @property {number} dropCounter - Frame counter for auto-drop
 * @property {number} dropInterval - Milliseconds between auto-drops
 * @property {object[]} garbageReceived - History of garbage received
 * @property {number} zoneMeter - Zone meter (0-100), increases by line clears
 * @property {boolean} zoneActive - Whether Zone is currently active
 * @property {number} zoneStartTime - Timestamp when Zone started
 * @property {number} zoneLineCount - Lines cleared during current Zone
 * @property {number} zoneTotalDuration - Total Zone duration in ms (20000ms = 20 seconds)
 */

/**
 * @typedef {Object} Spectator
 * @property {string} id - Socket.io connection ID
 * @property {string} name - Spectator's display name
 */

/**
 * Global lobby state - holds all game state server-side
 *
 * @type {Object}
 * @property {Map<string, Player>} players - Active players (socketId -> Player)
 * @property {Map<string, Spectator>} spectators - Spectators (socketId -> Spectator)
 * @property {boolean} gameStarted - Whether game is currently running
 * @property {NodeJS.Timeout|null} gameInterval - setInterval reference for game tick
 * @property {string[]} pieceQueue - Shared piece queue (7-bag system)
 * @property {Map<string, number>} playerSequenceIndex - Each player's position in piece queue
 */
const lobby = {
  players: new Map(),
  spectators: new Map(),
  gameStarted: false,
  gameInterval: null,
  pieceQueue: [],
  playerSequenceIndex: new Map()
};

const PORT = process.env.PORT || 3000;
const LOBBY_ROOM = 'global-lobby';

io.on('connection', (socket) => {
  console.log('Player connected:', socket.id);

  /**
   * Handle player joining the lobby
   * @event join-lobby
   * @param {object} data - Event data
   * @param {string} data.playerName - Name of the player joining
   */
  socket.on('join-lobby', ({ playerName }) => {
    // Add socket to lobby room so they receive lobby events
    socket.join(LOBBY_ROOM);
    socket.data.playerName = playerName;

    // If game is in progress, add as spectator instead
    if (lobby.gameStarted) {
      const spectator = {
        id: socket.id,
        name: playerName
      };
      lobby.spectators.set(socket.id, spectator);

      // Notify all clients about new spectator
      io.to(LOBBY_ROOM).emit('spectator-joined', {
        spectator: { id: spectator.id, name: spectator.name },
        spectators: getSpectatorsList()
      });

      // Send current game state to spectator only
      socket.emit('forced-spectator', {
        spectatorId: socket.id,
        states: getStates(),
        players: getPlayersList()
      });

      console.log(`${playerName} joined as spectator (${lobby.spectators.size} spectators)`);
      return;
    }

    const player = {
      id: socket.id,
      name: playerName,
      score: 0,
      lines: 0,
      level: 1,
      board: createEmptyBoard(),
      currentPiece: null,
      nextPiece: null,
      eliminated: false,
      ready: false,
      dropCounter: 0,
      dropInterval: 1000,
      garbageReceived: [],
      zoneMeter: 0,
      zoneActive: false,
      zoneStartTime: null,
      zoneLineCount: 0,
      zoneTotalDuration: 20000
    };

    lobby.players.set(socket.id, player);

    // Broadcast only to players in the lobby room
    io.to(LOBBY_ROOM).emit('player-joined', {
      player: { id: player.id, name: player.name },
      players: getPlayersList()
    });

    console.log(`${playerName} joined global lobby (${lobby.players.size} players)`);
  });

  /**
   * Handle player marking themselves as ready
   * @event ready
   */
  socket.on('ready', () => {
    const player = lobby.players.get(socket.id);
    if (!player) return;

    player.ready = true;

    // Broadcast only to players in the lobby room
    io.to(LOBBY_ROOM).emit('player-joined', {
      player: { id: player.id, name: player.name, ready: player.ready },
      players: getPlayersList()
    });

    // Check if all players ready and min 2 players (spectators don't count)
    if (lobby.players.size >= 2 && lobby.players.size <= 8) {
      const allReady = Array.from(lobby.players.values()).every(p => p.ready);
      if (allReady) {
        startGame();
      }
    }
  });

  /**
   * Handle player marking themselves as not ready
   * @event unready
   */
  socket.on('unready', () => {
    const player = lobby.players.get(socket.id);
    if (!player) return;

    player.ready = false;

    // Broadcast only to players in the lobby room
    io.to(LOBBY_ROOM).emit('player-joined', {
      player: { id: player.id, name: player.name, ready: player.ready },
      players: getPlayersList()
    });
  });

  /**
   * Handle player moving piece left or right
   * @event player-move
   * @param {object} data - Event data
   * @param {string} data.playerId - ID of the player
   * @param {'left'|'right'} data.direction - Direction to move
   */
  socket.on('player-move', ({ playerId, direction }) => {
    if (!lobby.gameStarted) return;

    const player = lobby.players.get(playerId);
    if (!player || player.eliminated || !player.currentPiece) return;

    const newX = player.currentPiece.x + (direction === 'left' ? -1 : 1);
    if (isValidPosition(player.currentPiece, newX, player.currentPiece.y, player.board)) {
      player.currentPiece.x = newX;
      broadcastState();
    }
  });

  socket.on('player-rotate', ({ playerId }) => {
    if (!lobby.gameStarted) return;

    const player = lobby.players.get(playerId);
    if (!player || player.eliminated || !player.currentPiece) return;

    const originalShape = player.currentPiece.shape;
    const rows = originalShape.length;
    const cols = originalShape[0].length;

    const rotated = Array(cols).fill(null).map(() => Array(rows).fill(0));
    for (let row = 0; row < rows; row++) {
      for (let col = 0; col < cols; col++) {
        rotated[col][rows - 1 - row] = originalShape[row][col];
      }
    }

    const kicks = [0, -1, 1, -2, 2];
    for (const kick of kicks) {
      if (isValidPosition(player.currentPiece, player.currentPiece.x + kick, player.currentPiece.y, player.board, rotated)) {
        player.currentPiece.shape = rotated;
        player.currentPiece.x += kick;
        broadcastState();
        return;
      }
    }
  });

  socket.on('player-drop', ({ playerId, hard }) => {
    if (!lobby.gameStarted) return;

    const player = lobby.players.get(playerId);
    if (!player || player.eliminated || !player.currentPiece) return;

    if (hard) {
      let dropped = 0;
      while (isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y + 1, player.board)) {
        player.currentPiece.y++;
        dropped++;
      }
      player.score += dropped * 2;
      lockPiece(player);
    } else {
      const newY = player.currentPiece.y + 1;
      if (isValidPosition(player.currentPiece, player.currentPiece.x, newY, player.board)) {
        player.currentPiece.y = newY;
        player.score += 1;
        broadcastState();
      } else {
        lockPiece(player);
      }
    }
  });

  socket.on('player-hold', ({ playerId }) => {
    if (!lobby.gameStarted) return;

    const player = lobby.players.get(playerId);
    if (!player || player.eliminated || !player.currentPiece) return;
    if (!player.canHold) return;

    if (player.holdPiece === null) {
      // First hold - store current piece and get next from queue
      player.holdPiece = {
        type: player.currentPiece.type,
        shape: JSON.parse(JSON.stringify(player.currentPiece.shape)),
        color: player.currentPiece.color
      };
      player.currentPiece = player.nextPiece;

      // Advance sequence index since we consumed nextPiece
      const idx = lobby.playerSequenceIndex.get(playerId);
      lobby.playerSequenceIndex.set(playerId, idx + 1);

      // Get new next piece from queue
      const nextType = lobby.pieceQueue[idx];
      if (nextType) {
        player.nextPiece = getPieceFromType(nextType);
      } else {
        // Regenerate queue if exhausted
        lobby.pieceQueue = createPieceQueue(14);
        player.nextPiece = getPieceFromType(lobby.pieceQueue[0]);
        lobby.playerSequenceIndex.set(playerId, 1);
      }
    } else {
      // Swap with held piece - current piece goes to hold, held piece becomes current
      // Don't advance sequence - the current piece is just going into hold
      const temp = {
        type: player.currentPiece.type,
        shape: JSON.parse(JSON.stringify(player.currentPiece.shape)),
        color: player.currentPiece.color
      };
      player.currentPiece = {
        ...player.holdPiece,
        shape: JSON.parse(JSON.stringify(player.holdPiece.shape)),
        x: Math.floor(BOARD_WIDTH / 2) - Math.floor(player.holdPiece.shape[0].length / 2),
        y: 0
      };
      player.holdPiece = temp;

      // Update nextPiece to be the piece that was just held
      player.nextPiece = {
        type: temp.type,
        shape: JSON.parse(JSON.stringify(temp.shape)),
        color: temp.color,
        x: Math.floor(BOARD_WIDTH / 2) - Math.floor(temp.shape[0].length / 2),
        y: 0
      };
    }

    player.canHold = false;
    broadcastState();
  });

  socket.on('zone-activate', ({ playerId }) => {
    if (!lobby.gameStarted) return;

    const player = lobby.players.get(playerId);
    if (!player || player.eliminated || player.zoneActive) return;

    // Only activate if Zone meter is full
    if (player.zoneMeter < 100) return;

    player.zoneActive = true;
    player.zoneStartTime = Date.now();
    player.zoneLineCount = 0;
    player.zoneMeter = 0;

    console.log(`[ZONE] ${player.name} activated Zone!`);
    broadcastState();
  });

  socket.on('disconnect', () => {
    const player = lobby.players.get(socket.id);
    if (player) {
      if (lobby.gameStarted) {
        player.eliminated = true;
        console.log(`[ELIMINATION] ${player.name} eliminated - player disconnected during game`);
        broadcastState();
        checkGameOver();
      }
      console.log(`${player.name} disconnected (${lobby.players.size - 1} remaining)`);

      lobby.players.delete(socket.id);

      io.to(LOBBY_ROOM).emit('player-left', {
        playerId: socket.id,
        players: getPlayersList()
      });

      // If lobby empty and game running, stop game
      if (lobby.players.size === 0 && lobby.gameStarted) {
        if (lobby.gameInterval) clearInterval(lobby.gameInterval);
        lobby.gameStarted = false;
      }
      return;
    }

    // Handle spectator disconnect
    const spectator = lobby.spectators.get(socket.id);
    if (spectator) {
      console.log(`${spectator.name} (spectator) disconnected`);
      lobby.spectators.delete(socket.id);

      io.to(LOBBY_ROOM).emit('spectator-left', {
        spectatorId: socket.id,
        spectators: getSpectatorsList()
      });
    }
  });
});

function getPlayersList() {
  return Array.from(lobby.players.values()).map(p => ({
    id: p.id,
    name: p.name,
    score: p.score,
    lines: p.lines,
    level: p.level,
    eliminated: p.eliminated,
    ready: p.ready
  }));
}

function getSpectatorsList() {
  return Array.from(lobby.spectators.values()).map(s => ({
    id: s.id,
    name: s.name
  }));
}

function getRandomPiece() {
  const key = TETROMINO_KEYS[Math.floor(Math.random() * TETROMINO_KEYS.length)];
  const tetromino = TETROMINOS[key];
  return {
    type: key,
    shape: JSON.parse(JSON.stringify(tetromino.shape)),
    color: tetromino.color,
    x: Math.floor(BOARD_WIDTH / 2) - Math.floor(tetromino.shape[0].length / 2),
    y: 0
  };
}

function spawnPiece(player) {
  const idx = lobby.playerSequenceIndex.get(player.id);
  const pieceType = lobby.pieceQueue[idx];

  if (!pieceType) {
    // Regenerate queue if exhausted
    lobby.pieceQueue = createPieceQueue(14);
    lobby.playerSequenceIndex.set(player.id, 0);
    return spawnPiece(player);
  }

  // Advance sequence index for next spawn
  lobby.playerSequenceIndex.set(player.id, idx + 1);

  player.currentPiece = player.nextPiece;
  player.nextPiece = getPieceFromType(pieceType);

  if (!player.currentPiece) return false;
  return isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y, player.board);
}

function lockPiece(player) {
  if (!player.currentPiece) return;

  // Lock piece into board
  for (let row = 0; row < player.currentPiece.shape.length; row++) {
    for (let col = 0; col < player.currentPiece.shape[row].length; col++) {
      if (player.currentPiece.shape[row][col]) {
        const boardY = player.currentPiece.y + row;
        const boardX = player.currentPiece.x + col;
        if (boardY >= 0 && boardY < BOARD_HEIGHT && boardX >= 0 && boardX < BOARD_WIDTH) {
          player.board[boardY][boardX] = player.currentPiece.color;
        }
      }
    }
  }

  const rowsCleared = clearRows(player);

  // Check if board overflowed (top 2 rows have blocks) - this means player is eliminated
  if (checkBoardOverflow(player.board)) {
    player.eliminated = true;
    console.log(`[ELIMINATION] ${player.name} eliminated - board overflowed`);
    broadcastState();
    checkGameOver();
    return;
  }

  const spawnResult = spawnPiece(player);
  if (!spawnResult) {
    player.eliminated = true;
    console.log(`[ELIMINATION] ${player.name} eliminated - piece could not spawn`);
  }

  // Reset canHold for the new piece
  player.canHold = true;

  if (rowsCleared > 0) sendGarbage(player, rowsCleared);

  broadcastState();
  checkGameOver();
}

function clearRows(player) {
  const result = clearRowsFromBoard(player.board);
  let rowsCleared = result.rowsCleared;
  
  // During Zone, lines cascade to bottom instead of disappearing - reapply them
  if (player.zoneActive && rowsCleared > 0) {
    player.zoneLineCount += rowsCleared;
    // Keep the lines on board for cascade effect (they stay, don't disappear)
    player.board = result.board;
    // Add cleared lines back to bottom for visual cascade
    for (let i = 0; i < rowsCleared; i++) {
      player.board.push(Array(BOARD_WIDTH).fill('#999999')); // Cleared line color
    }
    // Shift other lines up to make room
    player.board = player.board.slice(-BOARD_HEIGHT);
    return rowsCleared; // Still count for scoring
  }

  player.board = result.board;

  if (rowsCleared > 0) {
    player.lines += rowsCleared;
    const points = [0, 100, 300, 500, 800];
    player.score += points[rowsCleared] * player.level;
    player.level = Math.floor(player.lines / 10) + 1;
    player.dropInterval = Math.max(100, 1000 - (player.level - 1) * 100);
    
    // Fill Zone meter: different amounts based on lines cleared
    const zoneMeterIncrease = [0, 15, 25, 35, 50][Math.min(4, rowsCleared)] || 0;
    player.zoneMeter = Math.min(100, player.zoneMeter + zoneMeterIncrease);
  }
  
  return rowsCleared;
}

function sendGarbage(sender, rowsCleared) {
  // Number of garbage rows equals number of lines cleared
  const garbageLog = [];
  for (let i = 0; i < rowsCleared; i++) {
    // Re-compute opponents each iteration to exclude players eliminated by previous garbage
    const opponents = Array.from(lobby.players.values()).filter(p => p.id !== sender.id && !p.eliminated);
    if (opponents.length === 0) {
      break;
    }
    const target = opponents[Math.floor(Math.random() * opponents.length)];
    garbageLog.push(target.name);
    addGarbageToPlayer(target, sender.name);
  }

  if (garbageLog.length > 0) {
    console.log(`[GARBAGE] ${sender.name} cleared ${rowsCleared} row(s) -> sent garbage to: ${garbageLog.join(', ')}`);
  } else {
    console.log(`[GARBAGE] ${sender.name} cleared ${rowsCleared} row(s) but no opponents to send garbage to`);
  }
}

function sendZoneAttack(sender, zoneLineCount) {
  // Zone attack sends bonus garbage based on lines cleared during Zone
  // Formula: lines * 1.5 (rounded) = garbage rows
  // Example: 4 Zone lines = 6 garbage rows, 8 Zone lines = 12 garbage rows
  const totalGarbageLines = Math.round(zoneLineCount * 1.5);
  
  const garbageLog = [];
  for (let i = 0; i < totalGarbageLines; i++) {
    const opponents = Array.from(lobby.players.values()).filter(p => p.id !== sender.id && !p.eliminated);
    if (opponents.length === 0) break;
    
    const target = opponents[Math.floor(Math.random() * opponents.length)];
    garbageLog.push(target.name);
    addGarbageToPlayer(target, `${sender.name} (ZONE)`);
  }
  
  if (garbageLog.length > 0) {
    console.log(`[ZONE ATTACK] ${sender.name} sent ${totalGarbageLines} garbage rows from ${zoneLineCount} Zone lines -> targets: ${garbageLog.join(', ')}`);
  }
}

function addGarbageToPlayer(player, senderName) {
  // Use the pure function to add garbage row
  const result = addGarbageRow(player.board, player.currentPiece);
  player.board = result.board;
  player.currentPiece = result.currentPiece;

  // Track garbage received
  player.garbageReceived.push({ rows: 1, sender: senderName });
}

function startGame() {
  lobby.gameStarted = true;

  // Initialize shared piece queue
  lobby.pieceQueue = createPieceQueue(14); // 98 pieces (~14 bags)

  // Reset all player variables for the new game
  for (const player of lobby.players.values()) {
    player.board = createEmptyBoard();
    player.score = 0;
    player.lines = 0;
    player.level = 1;
    player.eliminated = false;
    player.dropInterval = 1000;
    player.dropCounter = 0;
    player.holdPiece = null;
    player.canHold = true;
    player.garbageReceived = [];
    player.currentPiece = null;
    player.nextPiece = null;
    player.zoneMeter = 0;
    player.zoneActive = false;
    player.zoneStartTime = null;
    player.zoneLineCount = 0;
    lobby.playerSequenceIndex.set(player.id, 0);
  }

  // Assign first two pieces from queue to all players
  const firstPieceType = lobby.pieceQueue[0];
  const secondPieceType = lobby.pieceQueue[1];

  for (const player of lobby.players.values()) {
    player.currentPiece = getPieceFromType(firstPieceType);
    player.nextPiece = getPieceFromType(secondPieceType);
    // Advance all players to index 2 since we've used first two pieces
    lobby.playerSequenceIndex.set(player.id, 2);
  }

  lobby.gameInterval = setInterval(() => gameTick(), 50);

  io.to(LOBBY_ROOM).emit('game-started', {
    players: getPlayersList(),
    states: getStates()
  });

  console.log(`Game started with ${lobby.players.size} players`);
}

function gameTick() {
  for (const player of lobby.players.values()) {
    if (player.eliminated || !player.currentPiece) continue;

    // Handle Zone time management
    if (player.zoneActive) {
      const elapsedTime = Date.now() - player.zoneStartTime;
      if (elapsedTime >= player.zoneTotalDuration) {
        // Zone ended - convert zone lines to attack
        player.zoneActive = false;
        console.log(`[ZONE END] ${player.name} cleared ${player.zoneLineCount} lines during Zone`);
        if (player.zoneLineCount > 0) {
          sendZoneAttack(player, player.zoneLineCount);
        }
        player.zoneLineCount = 0;
      } else {
        // During Zone - gravity doesn't apply, let pieces sort themselves out
        // Players still control manually, pieces still lock after a delay if touching stack
        continue;
      }
    }

    // Normal gravity (not in Zone)
    player.dropCounter += 50;
    if (player.dropCounter >= player.dropInterval) {
      player.dropCounter = 0;
      if (isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y + 1, player.board)) {
        player.currentPiece.y++;
      } else {
        lockPiece(player);
      }
    }
  }
  broadcastState();
  checkGameOver();
}

function broadcastState() {
  io.to(LOBBY_ROOM).emit('state-update', getStates());
}

function getStates() {
  return Array.from(lobby.players.values()).map(p => ({
    playerId: p.id,
    board: JSON.parse(JSON.stringify(p.board)),
    currentPiece: p.currentPiece ? JSON.parse(JSON.stringify(p.currentPiece)) : null,
    ghostY: p.currentPiece ? getGhostY(p.currentPiece, p.board) : null,
    nextPiece: p.nextPiece ? JSON.parse(JSON.stringify(p.nextPiece)) : null,
    holdPiece: p.holdPiece ? JSON.parse(JSON.stringify(p.holdPiece)) : null,
    canHold: p.canHold,
    score: p.score,
    lines: p.lines,
    level: p.level,
    eliminated: p.eliminated,
    sequenceIndex: lobby.playerSequenceIndex.get(p.id) || 0,
    garbageReceived: p.garbageReceived ? JSON.parse(JSON.stringify(p.garbageReceived)) : [],
    zoneMeter: p.zoneMeter,
    zoneActive: p.zoneActive,
    zoneTimeRemaining: p.zoneActive ? Math.max(0, p.zoneTotalDuration - (Date.now() - p.zoneStartTime)) : 0,
    zoneLineCount: p.zoneLineCount
  }));
}

function checkGameOver() {
  const activePlayers = Array.from(lobby.players.values()).filter(p => !p.eliminated);
  console.log(`[GAMEOVER CHECK] Active players: ${activePlayers.length} (${activePlayers.map(p => p.name).join(', ')})`);
  if (activePlayers.length <= 1) {
    console.log(`[GAME OVER] ${activePlayers.length === 1 ? `Winner: ${activePlayers[0].name}` : 'No winner (all eliminated)'}`);

    // Stop the game interval but keep game state visible for viewing
    if (lobby.gameInterval) {
      clearInterval(lobby.gameInterval);
      lobby.gameInterval = null;
    }
    lobby.gameStarted = false;

    // Set all players to not ready for next game (but keep their final state)
    for (const player of lobby.players.values()) {
      player.ready = false;
    }

    // Move spectators to players for next round
    for (const [id, spectator] of lobby.spectators.entries()) {
      const player = {
        id: spectator.id,
        name: spectator.name,
        score: 0,
        lines: 0,
        level: 1,
        board: createEmptyBoard(),
        currentPiece: null,
        nextPiece: null,
        holdPiece: null,
        canHold: true,
        eliminated: false,
        ready: false,
        dropCounter: 0,
        dropInterval: 1000,
        garbageReceived: []
      };
      lobby.players.set(id, player);
      lobby.playerSequenceIndex.set(id, 0);
      spectator.socket = io.sockets.sockets.get(id);
      if (spectator.socket) {
        spectator.socket.data.playerName = spectator.name;
      }
    }
    lobby.spectators.clear();

    // Broadcast game over with final states
    io.to(LOBBY_ROOM).emit('game-over', { states: getStates() });

    // Broadcast updated player list
    io.to(LOBBY_ROOM).emit('player-joined', {
      player: null,
      players: getPlayersList()
    });

    console.log(`Game over. Reset ${lobby.players.size} players for next round`);
  }
}

server.listen(PORT, () => {
  console.log(`Tetris Battle Royale server running on port ${PORT}`);
  console.log(`Open http://localhost:${PORT} in 2-8 browser tabs to play!`);
});