/**
 * Tank War PVP Server
 * WebSocket server for online 1v1 multiplayer.
 * Handles room management, message relay, and basic game state authority.
 *
 * Deployment note:
 *   - /health             → HTTP health check (used by K8s livenessProbe/readinessProbe)
 *   - /tankwar/ws → WebSocket upgrade path (exposed publicly via Nginx)
 *   Both share the same HTTP server on PORT.
 */

const { WebSocketServer } = require('ws');
const express = require('express');
const http = require('http');

// ============================================================
// Content Security Proxy Configuration// The content security service is now deployed as an independent
// microservice in the "content-security" K8s namespace.
// This proxy forwards /api/content/* requests to that service.
// ============================================================
const CONTENT_SECURITY_SERVICE_URL = process.env.CONTENT_SECURITY_SERVICE_URL
  || 'http://content-security-service.content-security.svc.cluster.local:3000';
const GAME_ID = process.env.GAME_ID || 'tankwar';
// ============================================================
// Configuration
// ============================================================
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '0.0.0.0';
const WS_PATH = process.env.WS_PATH || '/tankwar/ws';
const HEARTBEAT_INTERVAL = 10000; // ms
const ROOM_TIMEOUT = 300000; // 5 minutes room idle timeout

// ============================================================
// Express HTTP Server + Content Security API
// ============================================================
const app = express();
const server = http.createServer(app);

// Parse JSON bodies
app.use(express.json({ limit: '2mb' }));

// Health check endpoints (for K8s livenessProbe/readinessProbe)
app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    activeConnections: players.size,
    activeRooms: rooms.size,
    activeTeamRooms: teamRooms.size
  });
});
app.get('/tankwar/health', (req, res) => {
  res.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    activeConnections: players.size,
    activeRooms: rooms.size,
    activeTeamRooms: teamRooms.size
  });
});
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', timestamp: Date.now() });
});

// ============================================================
// Content Security Proxy
// Forward /api/content/* requests to the independent
// content-security-service in the content-security namespace.
// This allows the tankwar-server to delegate all content
// moderation to the shared microservice.
// ============================================================

app.use('/api/content', (req, res, next) => {
  // Parse target host and port
  const urlObj = new URL(CONTENT_SECURITY_SERVICE_URL);
  const targetPath = `/api/content${req.path}`;
  const qs = req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : '';
  const finalPath = qs ? `${targetPath}${qs}` : targetPath;

  const proxyReq = http.request({
    hostname: urlObj.hostname,
    port: urlObj.port || 80,
    path: finalPath,
    method: req.method,
    headers: {
      ...req.headers,
      host: `${urlObj.hostname}:${urlObj.port || 80}`,
      'X-Game-Id': GAME_ID,
      'X-Forwarded-For': req.ip || req.socket.remoteAddress,
    },
  }, (proxyRes) => {
    res.writeHead(proxyRes.statusCode, proxyRes.headers);
    proxyRes.pipe(res, { end: true });
  });

  proxyReq.on('error', (err) => {
    console.error('[TankWar Proxy] Failed to forward to content-security-service:', err.message);
    if (!res.headersSent) {
      res.status(502).json({
        errcode: -1,
        errmsg: '内容安全服务暂时不可用，请稍后再试',
      });
    }
  });

  // Pipe request body to proxy
  req.pipe(proxyReq, { end: true });
});

// ============================================================
// Message Types (must match client NET_MSG)
// ============================================================
const NET_MSG = {
  CREATE_ROOM: 'create_room',
  JOIN_ROOM: 'join_room',
  ROOM_CREATED: 'room_created',
  ROOM_JOINED: 'room_joined',
  ROOM_ERROR: 'room_error',
  OPPONENT_JOINED: 'opponent_joined',
  OPPONENT_LEFT: 'opponent_left',
  GAME_START: 'game_start',
  PLAYER_INPUT: 'player_input',
  PLAYER_STATE: 'player_state',
  BULLET_FIRE: 'bullet_fire',
  BULLET_HIT: 'bullet_hit',
  PLAYER_HIT: 'player_hit',
  PLAYER_KILLED: 'player_killed',
  GAME_OVER: 'game_over',
  PING: 'ping',
  PONG: 'pong',
  SYNC_STATE: 'sync_state',
  // Team (3v3)
  CREATE_TEAM: 'create_team',
  JOIN_TEAM: 'join_team',
  LEAVE_TEAM: 'leave_team',
  TEAM_READY: 'team_ready',
  TEAM_KICK: 'team_kick',
  TEAM_DISBAND: 'team_disband',
  TEAM_STATE: 'team_state',
  MATCH_START: 'match_start',
  MATCH_CANCEL: 'match_cancel',
  MATCH_FOUND: 'match_found',
  MATCH_TIMEOUT: 'match_timeout',
  BASE_HIT: 'base_hit',
  BASE_DESTROYED: 'base_destroyed',
  PLAYER_RESPAWN: 'player_respawn',
  TEAM_GAME_START: 'team_game_start',
  TEAM_GAME_OVER: 'team_game_over',
  TERRAIN_CHANGE: 'terrain_change',
  BOT_STATE: 'bot_state',
  RECONNECT: 'reconnect',
  RECONNECT_OK: 'reconnect_ok',
  PLAYER_DISCONNECT: 'player_disconnect',
  BOT_TAKEOVER: 'bot_takeover',
  SOLO_MATCH: 'solo_match',
  REMATCH: 'rematch',
  REMATCH_READY: 'rematch_ready',
};

// ============================================================
// 3v3 Configuration
// ============================================================
const TEAM_SIZE = 3;
const TEAM_MATCH_TIMEOUT = 6000; // 6s matchmaking timeout (testing)
const TEAM_RECONNECT_TIMEOUT = 60000; // 60s to reconnect

// ============================================================
// Battle Configuration (X vs X)
// ============================================================
const BATTLE_CONFIG = {
  '1v1': { teamSize: 1, baseHp: 5, fillWithBots: false },
  '2v2': { teamSize: 2, baseHp: 8, fillWithBots: true },
  '3v3': { teamSize: 3, baseHp: 10, fillWithBots: true },
};

// ============================================================
// Room Management
// ============================================================
/** @type {Map<string, Room>} */
const rooms = new Map();

/** @type {Map<WebSocket, PlayerInfo>} */
const players = new Map();

/** @type {Map<string, TeamRoom>} */
const teamRooms = new Map();

/** @type {Array<TeamRoom>} Matching pool for team queues */
const teamMatchPool = [];

/** @type {Array<WebSocket>} Matching pool for solo players */
const soloMatchPool = [];

class Room {
  constructor(id, host) {
    this.id = id;
    this.host = host; // WebSocket of player 1
    this.guest = null; // WebSocket of player 2
    this.state = 'waiting'; // waiting | playing | finished
    this.createdAt = Date.now();
    this.mapId = Math.floor(Math.random() * 3) + 1;
  }

  isFull() {
    return this.host && this.guest;
  }

  getOpponent(ws) {
    if (ws === this.host) return this.guest;
    if (ws === this.guest) return this.host;
    return null;
  }

  getPlayerSlot(ws) {
    if (ws === this.host) return 1;
    if (ws === this.guest) return 2;
    return 0;
  }

  removePlayer(ws) {
    if (ws === this.host) this.host = null;
    if (ws === this.guest) this.guest = null;
  }

  isEmpty() {
    return !this.host && !this.guest;
  }
}

class PlayerInfo {
  constructor(ws, playerId) {
    this.ws = ws;
    this.playerId = playerId;
    this.nickname = '';
    this.avatarUrl = '';
    this.skinId = ''; // equipped tank skin id
    this.roomId = null;
    this.teamId = null;
    this.isAlive = true;
    this.lastPing = Date.now();
  }
}

// ============================================================
// TeamRoom - 3v3 Team Room Management
// ============================================================

class TeamRoom {
  /**
   * @param {string} id - Unique team room id
   * @param {WebSocket} leaderWs - WebSocket of the team leader
   * @param {string} leaderId - Player id of the leader
   * @param {string} [battleMode='3v3'] - Battle mode ('1v1' or '3v3')
   * @param {string} [leaderNickname=''] - Display nickname of the leader
   */
  constructor(id, leaderWs, leaderId, battleMode = '3v3', leaderNickname = '', leaderAvatarUrl = '', leaderSkinId = '') {
    this.id = id;
    this.state = 'forming'; // forming | matching | playing | finished
    this.createdAt = Date.now();
    this.mapId = Math.floor(Math.random() * 3) + 1;
    this.battleMode = battleMode;

    const config = BATTLE_CONFIG[battleMode] || BATTLE_CONFIG['3v3'];
    this.teamSize = config.teamSize;
    this.fillWithBotsEnabled = config.fillWithBots;

    // Team A members: { ws, playerId, nickname, avatarUrl, skinId, ready, isBot, disconnectedAt }
    this.teamA = [{ ws: leaderWs, playerId: leaderId, nickname: leaderNickname || '', avatarUrl: leaderAvatarUrl || '', skinId: leaderSkinId || '', ready: true, isBot: false, disconnectedAt: null }];
    // Team B members
    this.teamB = [];
    this.leaderId = leaderId;

    // Matching state
    this.matchStartTime = null;
    this.matchTimer = null;

    // Game state
    this.teamABaseHp = config.baseHp;
    this.teamBBaseHp = config.baseHp;
    this.gameStartTime = null;
  }

  /** Get all members of team A */
  getTeamAMembers() {
    return this.teamA;
  }

  /** Get all members of team B */
  getTeamBMembers() {
    return this.teamB;
  }

  /** Get all human (non-bot) members across both teams */
  getAllHumanMembers() {
    return [...this.teamA, ...this.teamB].filter(m => !m.isBot);
  }

  /** Get all members across both teams */
  getAllMembers() {
    return [...this.teamA, ...this.teamB];
  }

  /** Find which team a player belongs to */
  getPlayerTeam(playerId) {
    if (this.teamA.find(m => m.playerId === playerId)) return 'A';
    if (this.teamB.find(m => m.playerId === playerId)) return 'B';
    return null;
  }

  /** Find a member by playerId */
  getMember(playerId) {
    return this.getAllMembers().find(m => m.playerId === playerId) || null;
  }

  /** Find a member by WebSocket */
  getMemberByWs(ws) {
    return this.getAllMembers().find(m => m.ws === ws) || null;
  }

  /** Check if team A is full */
  isTeamAFull() {
    return this.teamA.length >= this.teamSize;
  }

  /** Check if team B is full */
  isTeamBFull() {
    return this.teamB.length >= this.teamSize;
  }

  /** Check if both teams are full */
  isFull() {
    return this.teamA.length >= this.teamSize && this.teamB.length >= this.teamSize;
  }

  /** Check if all team A members are ready */
  isTeamAReady() {
    return this.teamA.length > 0 && this.teamA.every(m => m.ready || m.isBot);
  }

  /** Add a player to team A */
  addToTeamA(ws, playerId, nickname = '', avatarUrl = '', skinId = '') {
    if (this.teamA.length >= this.teamSize) return false;
    // Prevent duplicate playerId across both teams
    if (this.teamA.some(m => m.playerId === playerId) || this.teamB.some(m => m.playerId === playerId)) return false;
    this.teamA.push({ ws, playerId, nickname, avatarUrl, skinId, ready: false, isBot: false, disconnectedAt: null });
    return true;
  }

  addToTeamB(ws, playerId, nickname = '', avatarUrl = '', skinId = '') {
    if (this.teamB.length >= this.teamSize) return false;
    // Prevent duplicate playerId across both teams
    if (this.teamA.some(m => m.playerId === playerId) || this.teamB.some(m => m.playerId === playerId)) return false;
    this.teamB.push({ ws, playerId, nickname, avatarUrl, skinId, ready: false, isBot: false, disconnectedAt: null });
    return true;
  }
  /** Remove a player from the team room */
  removePlayer(playerId) {
    this.teamA = this.teamA.filter(m => m.playerId !== playerId);
    this.teamB = this.teamB.filter(m => m.playerId !== playerId);
  }

  /** Fill remaining slots with AI bots */
  fillWithBots() {
    let botCounter = 0;
    while (this.teamA.length < this.teamSize) {
      botCounter++;
      this.teamA.push({
        ws: null,
        playerId: `bot_a_${botCounter}_${this.id}`,
        nickname: '',
        avatarUrl: '',
        ready: true,
        isBot: true,
        disconnectedAt: null,
      });
    }
    while (this.teamB.length < this.teamSize) {
      botCounter++;
      this.teamB.push({
        ws: null,
        playerId: `bot_b_${botCounter}_${this.id}`,
        nickname: '',
        avatarUrl: '',
        ready: true,
        isBot: true,
        disconnectedAt: null,
      });
    }
  }

  /** Broadcast a message to all human members in the room */
  broadcast(type, data, excludeWs = null) {
    for (const member of this.getAllHumanMembers()) {
      if (member.ws && member.ws !== excludeWs && member.ws.readyState === 1) {
        sendMessage(member.ws, type, data);
      }
    }
  }

  /**
   * Reset the room for a rematch: clear game state, keep players.
   * Returns the room to 'forming' state so a new game can start.
   */
  resetForRematch() {
    this.state = 'forming';
    this.mapId = Math.floor(Math.random() * 3) + 1;
    this.matchStartTime = null;
    if (this.matchTimer) {
      clearTimeout(this.matchTimer);
      this.matchTimer = null;
    }

    const config = BATTLE_CONFIG[this.battleMode] || BATTLE_CONFIG['3v3'];
    this.teamABaseHp = config.baseHp;
    this.teamBBaseHp = config.baseHp;
    this.gameStartTime = null;

    // Reset rematch tracking
    this._rematchPlayers = new Set();
    this._rematchTimer = null;

    // Reset ready state for all human members
    for (const member of this.getAllHumanMembers()) {
      member.disconnectedAt = null;
      member.isBot = false;
    }
  }

  /** Broadcast to all members of a specific team */
  broadcastToTeam(team, type, data, excludeWs = null) {
    const members = team === 'A' ? this.teamA : this.teamB;
    for (const member of members) {
      if (member.ws && !member.isBot && member.ws !== excludeWs && member.ws.readyState === 1) {
        sendMessage(member.ws, type, data);
      }
    }
  }

  /** Get serializable team state for broadcasting */
  getTeamState() {
    return {
      teamId: this.id,
      state: this.state,
      leaderId: this.leaderId,
      battleMode: this.battleMode,
      teamSize: this.teamSize,
      teamA: this.teamA.map(m => ({
        playerId: m.playerId,
        nickname: m.nickname || '',
        avatarUrl: m.avatarUrl || '',
        skinId: m.skinId || '',
        ready: m.ready,
        isBot: m.isBot,
        isLeader: m.playerId === this.leaderId,
        connected: m.isBot || (m.ws && m.ws.readyState === 1),
      })),
      teamB: this.teamB.map(m => ({
        playerId: m.playerId,
        nickname: m.nickname || '',
        avatarUrl: m.avatarUrl || '',
        skinId: m.skinId || '',
        ready: m.ready,
        isBot: m.isBot,
        connected: m.isBot || (m.ws && m.ws.readyState === 1),
      })),
      teamABaseHp: this.teamABaseHp,
      teamBBaseHp: this.teamBBaseHp,
    };
  }
}

// ============================================================
// Utility Functions
// ============================================================

/**
 * Generate a random room code (4-6 digit number).
 * @returns {string}
 */
function generateRoomCode() {
  let code;
  do {
    code = String(Math.floor(1000 + Math.random() * 9000)); // 4-digit code
  } while (rooms.has(code));
  return code;
}

/**
 * Send a JSON message to a WebSocket client.
 * @param {WebSocket} ws
 * @param {string} type
 * @param {object} data
 */
function sendMessage(ws, type, data = {}) {
  if (!ws || ws.readyState !== 1) return; // 1 = OPEN
  try {
    ws.send(JSON.stringify({ type, data, timestamp: Date.now() }));
  } catch (e) {
    console.error('[Server] Send error:', e.message);
  }
}

/**
 * Relay a message from one player to their opponent.
 * @param {WebSocket} senderWs
 * @param {string} type
 * @param {object} data
 */
function relayToOpponent(senderWs, type, data) {
  const playerInfo = players.get(senderWs);
  if (!playerInfo || !playerInfo.roomId) return;

  const room = rooms.get(playerInfo.roomId);
  if (!room) return;

  const opponent = room.getOpponent(senderWs);
  if (opponent) {
    sendMessage(opponent, type, data);
  }
}

/**
 * Generate a unique team room id.
 * @returns {string}
 */
function generateTeamId() {
  let id;
  do {
    id = 'T' + String(Math.floor(10000 + Math.random() * 90000));
  } while (teamRooms.has(id));
  return id;
}

/**
 * Relay a message from one player to all other players in the same team room.
 * @param {WebSocket} senderWs
 * @param {string} type
 * @param {object} data
 */
function relayToTeamRoom(senderWs, type, data) {
  const playerInfo = players.get(senderWs);
  if (!playerInfo || !playerInfo.teamId) return;

  const teamRoom = teamRooms.get(playerInfo.teamId);
  if (!teamRoom) return;

  teamRoom.broadcast(type, data, senderWs);
}

// ============================================================
// Message Handlers
// ============================================================

function handleCreateRoom(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo) return;

  // Leave current room/team if any
  if (playerInfo.roomId) {
    leaveRoom(ws);
  }
  if (playerInfo.teamId) {
    handleLeaveTeam(ws, {});
  }

  const roomCode = generateRoomCode();

  // Create a TeamRoom in 1v1 mode instead of a legacy Room
const teamRoom = new TeamRoom(roomCode, ws, playerInfo.playerId, '1v1', playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
  teamRooms.set(roomCode, teamRoom);
  playerInfo.teamId = roomCode;

  console.log(`[Server] 1v1 Room ${roomCode} created by ${playerInfo.playerId} (using TeamRoom)`);

  sendMessage(ws, NET_MSG.ROOM_CREATED, {
    roomId: roomCode,
    roomCode: roomCode,
    playerSlot: 1,
  });
}

function handleJoinRoom(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo) return;

  const roomId = data.roomId;
  if (!roomId) {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Room code is required' });
    return;
  }

  // Look up in teamRooms (1v1 rooms are now TeamRooms)
  const teamRoom = teamRooms.get(roomId);
  if (!teamRoom) {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Room not found' });
    return;
  }

  if (teamRoom.battleMode !== '1v1') {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Room not found' });
    return;
  }

  if (teamRoom.teamB.length >= teamRoom.teamSize) {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Room is full' });
    return;
  }

  // Leave current room/team if any
  if (playerInfo.roomId) {
    leaveRoom(ws);
  }
  if (playerInfo.teamId) {
    handleLeaveTeam(ws, {});
  }

  // Join as team B
    teamRoom.addToTeamB(ws, playerInfo.playerId, playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
  playerInfo.teamId = roomId;

  console.log(`[Server] Player ${playerInfo.playerId} joined 1v1 room ${roomId} (TeamRoom)`);

  // Notify the joiner
  sendMessage(ws, NET_MSG.ROOM_JOINED, {
    roomId: roomId,
    roomCode: roomId,
    playerSlot: 2,
  });

  // Notify the host
  const host = teamRoom.teamA[0];
  if (host && host.ws) {
    sendMessage(host.ws, NET_MSG.OPPONENT_JOINED, {
      playerId: playerInfo.playerId,
    });
  }

  // Start game after countdown
  teamRoom.state = 'playing';

  setTimeout(() => {
    if (teamRoom.state === 'playing' && teamRoom.teamA.length > 0 && teamRoom.teamB.length > 0) {
      startTeamGame(teamRoom);
    }
  }, 3500); // 3.5s countdown
}

function leaveRoom(ws) {
  const playerInfo = players.get(ws);
  if (!playerInfo || !playerInfo.roomId) return;

  const room = rooms.get(playerInfo.roomId);
  if (!room) {
    playerInfo.roomId = null;
    return;
  }

  const opponent = room.getOpponent(ws);
  room.removePlayer(ws);
  playerInfo.roomId = null;

  // Notify opponent
  if (opponent) {
    sendMessage(opponent, NET_MSG.OPPONENT_LEFT, {});
  }

  // Clean up empty room
  if (room.isEmpty()) {
    rooms.delete(room.id);
    console.log(`[Server] Room ${room.id} deleted (empty)`);
  }
}

// ============================================================
// 3v3 Team Message Handlers
// ============================================================

function handleCreateTeam(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo) return;

  // Leave current team if any
  if (playerInfo.teamId) {
    handleLeaveTeam(ws, {});
  }

  // Read battleMode from client data, fall back to '3v3' if not provided
  const battleMode = (data && data.battleMode) || '3v3';

  const teamId = generateTeamId();
  const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, battleMode, playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
  teamRooms.set(teamId, teamRoom);
  playerInfo.teamId = teamId;

  console.log(`[Server] Team ${teamId} created by ${playerInfo.playerId} (mode: ${battleMode})`);

  sendMessage(ws, NET_MSG.TEAM_STATE, teamRoom.getTeamState());
}

function handleJoinTeam(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo) return;

  const teamId = data.teamId;
  if (!teamId) {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Team ID is required' });
    return;
  }

  let teamRoom = teamRooms.get(teamId);
  if (!teamRoom) {
    // Team was cleaned up (e.g. leader disconnected during dev-tool reload).
    // Auto-create a new room with the same ID so the invite link still works.
    console.log(`[Server] Team ${teamId} not found, auto-creating for ${playerInfo.playerId}`);
    teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, (data && data.battleMode) || '3v3', playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
    teamRooms.set(teamId, teamRoom);
    playerInfo.teamId = teamId;
    sendMessage(ws, NET_MSG.TEAM_STATE, teamRoom.getTeamState());
    return;
  }

  if (teamRoom.state !== 'forming') {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Team is not accepting members' });
    return;
  }

  if (teamRoom.isTeamAFull()) {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Team is full' });
    return;
  }

  // Leave current team if any
  if (playerInfo.teamId) {
    handleLeaveTeam(ws, {});
  }

  teamRoom.addToTeamA(ws, playerInfo.playerId, playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
  playerInfo.teamId = teamId;

  console.log(`[Server] Player ${playerInfo.playerId} joined team ${teamId}`);

  // Broadcast updated team state to all members
  teamRoom.broadcast(NET_MSG.TEAM_STATE, teamRoom.getTeamState());
}

function handleLeaveTeam(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo || !playerInfo.teamId) return;

  const teamRoom = teamRooms.get(playerInfo.teamId);
  if (!teamRoom) {
    playerInfo.teamId = null;
    return;
  }

  // If team is matching and leader leaves, cancel match first
  const wasLeader = teamRoom.leaderId === playerInfo.playerId;
  if (teamRoom.state === 'matching' && wasLeader) {
    cancelMatch(teamRoom);
  }

  teamRoom.removePlayer(playerInfo.playerId);
  playerInfo.teamId = null;

  // Remove from solo match pool if present
  const soloIdx = soloMatchPool.indexOf(ws);
  if (soloIdx !== -1) soloMatchPool.splice(soloIdx, 1);

  console.log(`[Server] Player ${playerInfo.playerId} left team ${teamRoom.id}`);

  // If leader left, assign new leader or disband
  if (wasLeader) {
    const remainingHumans = teamRoom.teamA.filter(m => !m.isBot);
    if (remainingHumans.length > 0) {
      teamRoom.leaderId = remainingHumans[0].playerId;
      console.log(`[Server] New leader for team ${teamRoom.id}: ${teamRoom.leaderId}`);
      teamRoom.broadcast(NET_MSG.TEAM_STATE, teamRoom.getTeamState());
    } else {
      // No humans left, disband
      cleanupTeamRoom(teamRoom.id);
    }
  } else {
    teamRoom.broadcast(NET_MSG.TEAM_STATE, teamRoom.getTeamState());
  }
}

function handleTeamReady(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo || !playerInfo.teamId) return;

  const teamRoom = teamRooms.get(playerInfo.teamId);
  if (!teamRoom || teamRoom.state !== 'forming') return;

  const member = teamRoom.getMember(playerInfo.playerId);
  if (!member) return;

  // Leader is always ready; toggle ready for non-leaders
  if (playerInfo.playerId !== teamRoom.leaderId) {
    member.ready = data.ready !== undefined ? !!data.ready : !member.ready;
  }

  teamRoom.broadcast(NET_MSG.TEAM_STATE, teamRoom.getTeamState());
}

function handleTeamKick(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo || !playerInfo.teamId) return;

  const teamRoom = teamRooms.get(playerInfo.teamId);
  if (!teamRoom) return;

  // Only leader can kick, and only during forming state
  if (teamRoom.leaderId !== playerInfo.playerId) {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Only the leader can kick players' });
    return;
  }

  if (teamRoom.state !== 'forming') {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Cannot kick during matching or game' });
    return;
  }

  const targetId = data.playerId;
  if (!targetId || targetId === playerInfo.playerId) return;

  const targetMember = teamRoom.getMember(targetId);
  if (!targetMember) return;

  // Notify the kicked player
  if (targetMember.ws && targetMember.ws.readyState === 1) {
    sendMessage(targetMember.ws, NET_MSG.TEAM_DISBAND, { reason: 'kicked' });
    const targetInfo = players.get(targetMember.ws);
    if (targetInfo) targetInfo.teamId = null;
  }

  teamRoom.removePlayer(targetId);
  console.log(`[Server] Player ${targetId} kicked from team ${teamRoom.id}`);

  teamRoom.broadcast(NET_MSG.TEAM_STATE, teamRoom.getTeamState());
}

function handleTeamDisband(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo || !playerInfo.teamId) return;

  const teamRoom = teamRooms.get(playerInfo.teamId);
  if (!teamRoom) return;

  // Only leader can disband
  if (teamRoom.leaderId !== playerInfo.playerId) {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Only the leader can disband' });
    return;
  }

  console.log(`[Server] Team ${teamRoom.id} disbanded by leader`);

  // Notify all members
  teamRoom.broadcast(NET_MSG.TEAM_DISBAND, { reason: 'disbanded' });

  // Clear teamId for all human members
  for (const member of teamRoom.getAllHumanMembers()) {
    if (member.ws) {
      const info = players.get(member.ws);
      if (info) info.teamId = null;
    }
  }

  cleanupTeamRoom(teamRoom.id);
}

function handleMatchStart(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo || !playerInfo.teamId) return;

  const teamRoom = teamRooms.get(playerInfo.teamId);
  if (!teamRoom) return;

  // Only leader can start matching
  if (teamRoom.leaderId !== playerInfo.playerId) {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Only the leader can start matching' });
    return;
  }

  if (teamRoom.state !== 'forming') return;

  // Check all team A members are ready
  if (!teamRoom.isTeamAReady()) {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Not all team members are ready' });
    return;
  }

  teamRoom.state = 'matching';
  teamRoom.matchStartTime = Date.now();

  // Add to match pool (dedup guard)
  if (!teamMatchPool.includes(teamRoom)) {
    teamMatchPool.push(teamRoom);
  }

  console.log(`[Server] Team ${teamRoom.id} entered matching pool`);

  // Broadcast matching state
  teamRoom.broadcast(NET_MSG.TEAM_STATE, teamRoom.getTeamState());

  // Set match timeout
  teamRoom.matchTimer = setTimeout(() => {
    if (teamRoom.state === 'matching') {
      handleMatchTimeout(teamRoom);
    }
  }, TEAM_MATCH_TIMEOUT);

  // Try to match immediately
  tryMatchTeams();
}

function handleMatchCancel(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo || !playerInfo.teamId) return;

  const teamRoom = teamRooms.get(playerInfo.teamId);
  if (!teamRoom || teamRoom.state !== 'matching') return;

  // Only leader can cancel
  if (teamRoom.leaderId !== playerInfo.playerId) return;

  cancelMatch(teamRoom);
}

function handleSoloMatch(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo) return;

  // Leave current team if any
  if (playerInfo.teamId) {
    handleLeaveTeam(ws, {});
  }

  // Read battleMode from client data, fall back to '3v3' if not provided
  const battleMode = (data && data.battleMode) || '3v3';

  // Create a solo team room for this player
  const teamId = generateTeamId();
  const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, battleMode, playerInfo.nickname || '', playerInfo.avatarUrl || '', playerInfo.skinId || '');
  teamRoom.state = 'matching';
  teamRoom.matchStartTime = Date.now();
  teamRooms.set(teamId, teamRoom);
  playerInfo.teamId = teamId;

  // Add to solo match pool (dedup guard)
  if (!soloMatchPool.includes(ws)) {
    soloMatchPool.push(ws);
  }

  console.log(`[Server] Player ${playerInfo.playerId} entered solo match pool (team ${teamId})`);

  sendMessage(ws, NET_MSG.TEAM_STATE, teamRoom.getTeamState());

  // Set match timeout
  teamRoom.matchTimer = setTimeout(() => {
    if (teamRoom.state === 'matching') {
      handleMatchTimeout(teamRoom);
    }
  }, TEAM_MATCH_TIMEOUT);

  // Try to match
  tryMatchTeams();
}

function cancelMatch(teamRoom) {
  if (teamRoom.matchTimer) {
    clearTimeout(teamRoom.matchTimer);
    teamRoom.matchTimer = null;
  }

  teamRoom.state = 'forming';
  teamRoom.matchStartTime = null;

  // Remove from match pool
  const idx = teamMatchPool.indexOf(teamRoom);
  if (idx !== -1) teamMatchPool.splice(idx, 1);

  // Remove solo players from solo pool
  for (const member of teamRoom.teamA) {
    const soloIdx = soloMatchPool.indexOf(member.ws);
    if (soloIdx !== -1) soloMatchPool.splice(soloIdx, 1);
  }

  console.log(`[Server] Match cancelled for team ${teamRoom.id}`);
  teamRoom.broadcast(NET_MSG.TEAM_STATE, teamRoom.getTeamState());
}

function handleMatchTimeout(teamRoom) {
  console.log(`[Server] Match timeout for team ${teamRoom.id}, filling with bots`);

  // Remove from match pool
  const idx = teamMatchPool.indexOf(teamRoom);
  if (idx !== -1) teamMatchPool.splice(idx, 1);

  // Fill remaining slots with bots
  teamRoom.fillWithBots();

  // Start the game
  startTeamGame(teamRoom);
}

/**
 * Try to match teams from the pool.
 * Simple matching: pair two teams or combine solo players.
 */
function tryMatchTeams() {
  // Try to pair two team queues
  if (teamMatchPool.length >= 2) {
    const teamA_room = teamMatchPool.shift();
    const teamB_room = teamMatchPool.shift();

    // Merge team B room members into team A room as opponents
    // Both teamA and teamB of teamB_room should be moved
    const allBMembers = [...teamB_room.teamA, ...teamB_room.teamB];
    for (const member of allBMembers) {
      teamA_room.addToTeamB(member.ws, member.playerId, member.nickname || '', member.avatarUrl || '', member.skinId || '');
      if (member.ws) {
        const info = players.get(member.ws);
        if (info) info.teamId = teamA_room.id;
      }
    }

    // Clean up team B room
    if (teamB_room.matchTimer) clearTimeout(teamB_room.matchTimer);
    teamRooms.delete(teamB_room.id);

    // Fill remaining with bots if needed
    teamA_room.fillWithBots();

    console.log(`[Server] Teams matched: ${teamA_room.id}`);
    startTeamGame(teamA_room);
    return;
  }

  // Try to combine solo players into teams
  // Collect all solo players that are in matching state
  const availableSolos = soloMatchPool.filter(ws => {
    const info = players.get(ws);
    if (!info || !info.teamId) return false;
    const room = teamRooms.get(info.teamId);
    return room && room.state === 'matching';
  });

  if (availableSolos.length >= 2) {
    // Deduplicate by playerId — same player should not appear twice
    const seenIds = new Set();
    const gamePlayersRaw = availableSolos.splice(0, Math.min(10, availableSolos.length));
    const gamePlayers = gamePlayersRaw.filter(ws => {
      const info = players.get(ws);
      if (!info || seenIds.has(info.playerId)) return false;
      seenIds.add(info.playerId);
      return true;
    });

    // Remove from solo pool
    for (const ws of gamePlayers) {
      const idx = soloMatchPool.indexOf(ws);
      if (idx !== -1) soloMatchPool.splice(idx, 1);
    }

    // Use the first player's team room as the game room
    const firstInfo = players.get(gamePlayers[0]);
    const gameRoom = teamRooms.get(firstInfo.teamId);

    // Clear match timer
    if (gameRoom.matchTimer) {
      clearTimeout(gameRoom.matchTimer);
      gameRoom.matchTimer = null;
    }

    // Add remaining players, alternating teams
    for (let i = 1; i < gamePlayers.length; i++) {
      const ws = gamePlayers[i];
      const info = players.get(ws);
      if (!info) continue;

      // Clean up their old solo team room
      const oldRoom = teamRooms.get(info.teamId);
      if (oldRoom && oldRoom.id !== gameRoom.id) {
        if (oldRoom.matchTimer) clearTimeout(oldRoom.matchTimer);
        teamRooms.delete(oldRoom.id);
      }

      info.teamId = gameRoom.id;

      // Alternate teams: first remaining player → teamB, next → teamA, etc.
      // (player at index 0 is already in teamA as the room creator)
      const isTeamBSlot = (i % 2 === 1);
      if (isTeamBSlot && !gameRoom.isTeamBFull()) {
        gameRoom.addToTeamB(ws, info.playerId, info.nickname || '', info.avatarUrl || '', info.skinId || '');
      } else if (!isTeamBSlot && !gameRoom.isTeamAFull()) {
        gameRoom.addToTeamA(ws, info.playerId, info.nickname || '', info.avatarUrl || '', info.skinId || '');
      } else if (!gameRoom.isTeamBFull()) {
        gameRoom.addToTeamB(ws, info.playerId, info.nickname || '', info.avatarUrl || '', info.skinId || '');
      } else {
        gameRoom.addToTeamA(ws, info.playerId, info.nickname || '', info.avatarUrl || '', info.skinId || '');
      }
    }

    // Fill with bots
    gameRoom.fillWithBots();

    console.log(`[Server] Solo players matched into team ${gameRoom.id}`);
    
    // Send MATCH_FOUND to all players before starting the game
    gameRoom.broadcast(NET_MSG.MATCH_FOUND, {});
    
    startTeamGame(gameRoom);
  }
}

/**
 * Start a team game (supports both 1v1 and 3v3).
 * @param {TeamRoom} teamRoom
 */
function startTeamGame(teamRoom) {
  teamRoom.state = 'playing';
  teamRoom.gameStartTime = Date.now();

  const config = BATTLE_CONFIG[teamRoom.battleMode] || BATTLE_CONFIG['3v3'];
  teamRoom.teamABaseHp = config.baseHp;
  teamRoom.teamBBaseHp = config.baseHp;

  if (teamRoom.matchTimer) {
    clearTimeout(teamRoom.matchTimer);
    teamRoom.matchTimer = null;
  }

  const gameData = {
    mapId: teamRoom.mapId,
    teamA: teamRoom.teamA.map(m => ({ playerId: m.playerId, nickname: m.nickname || '', isBot: m.isBot, skinId: m.skinId || '' })),
    teamB: teamRoom.teamB.map(m => ({ playerId: m.playerId, nickname: m.nickname || '', isBot: m.isBot, skinId: m.skinId || '' })),
    teamABaseHp: teamRoom.teamABaseHp,
    teamBBaseHp: teamRoom.teamBBaseHp,
    battleMode: teamRoom.battleMode,
    roomId: teamRoom.id,
  };

  console.log(`[Server] ${teamRoom.battleMode} game starting in room ${teamRoom.id}`);
  console.log(`[Server] teamA: ${JSON.stringify(gameData.teamA.map(m => ({ playerId: m.playerId, isBot: m.isBot })))}`);
  console.log(`[Server] teamB: ${JSON.stringify(gameData.teamB.map(m => ({ playerId: m.playerId, isBot: m.isBot })))}`);

  // For 1v1, use GAME_START message (client RoomScene listens for it)
  // For 3v3, use TEAM_GAME_START message (client TeamRoomScene listens for it)
  if (teamRoom.battleMode === '1v1') {
    // Send immediately (countdown already happened in handleJoinRoom)
    teamRoom.broadcast(NET_MSG.GAME_START, gameData);
  } else {
    // Notify all players with a short delay for loading
    setTimeout(() => {
      teamRoom.broadcast(NET_MSG.TEAM_GAME_START, gameData);
    }, 3000);
  }
}

/**
 * End a team game (supports both 1v1 and 3v3).
 * @param {TeamRoom} teamRoom
 * @param {string} reason - 'base_destroyed' (only valid reason)
 */
function endTeamGame(teamRoom, reason) {
  if (teamRoom.state !== 'playing') return;
  teamRoom.state = 'finished';

  // Determine winner: the team whose base is destroyed loses
  let winner = '';
  if (teamRoom.teamABaseHp <= 0) winner = 'B';
  else if (teamRoom.teamBBaseHp <= 0) winner = 'A';

  const resultData = {
    winner,
    reason: 'base_destroyed',
    teamABaseHp: teamRoom.teamABaseHp,
    teamBBaseHp: teamRoom.teamBBaseHp,
    battleMode: teamRoom.battleMode,
  };

  console.log(`[Server] ${teamRoom.battleMode} game ended in room ${teamRoom.id}, winner: ${winner}, reason: base_destroyed`);

  // Use appropriate message type
  if (teamRoom.battleMode === '1v1') {
    teamRoom.broadcast(NET_MSG.GAME_OVER, resultData);
  } else {
    teamRoom.broadcast(NET_MSG.TEAM_GAME_OVER, resultData);
  }

  // Initialize rematch tracking
  teamRoom._rematchPlayers = new Set();

  // Set a cleanup timer: if no rematch within 60s, clean up
  teamRoom._rematchTimer = setTimeout(() => {
    if (teamRoom.state === 'finished') {
      cleanupTeamRoom(teamRoom.id);
    }
  }, 60000); // 60s for result screen + rematch window
}

function handleBaseHit(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo || !playerInfo.teamId) return;

  const teamRoom = teamRooms.get(playerInfo.teamId);
  if (!teamRoom || teamRoom.state !== 'playing') return;

  const { targetTeam, damage } = data;
  const dmg = Math.min(damage || 1, 3); // Cap damage to prevent abuse

  // Validate targetTeam value
  if (targetTeam !== 'A' && targetTeam !== 'B') return;

  if (targetTeam === 'A') {
    teamRoom.teamABaseHp = Math.max(0, teamRoom.teamABaseHp - dmg);
  } else if (targetTeam === 'B') {
    teamRoom.teamBBaseHp = Math.max(0, teamRoom.teamBBaseHp - dmg);
  }

  // Broadcast base hit to all players
  teamRoom.broadcast(NET_MSG.BASE_HIT, {
    targetTeam,
    damage: dmg,
    teamABaseHp: teamRoom.teamABaseHp,
    teamBBaseHp: teamRoom.teamBBaseHp,
  });

  // Check if base destroyed
  if (teamRoom.teamABaseHp <= 0 || teamRoom.teamBBaseHp <= 0) {
    endTeamGame(teamRoom, 'base_destroyed');
  }
}

function handleTeamPlayerDisconnect(ws) {
  const playerInfo = players.get(ws);
  if (!playerInfo || !playerInfo.teamId) return;

  const teamRoom = teamRooms.get(playerInfo.teamId);
  if (!teamRoom) return;

  const member = teamRoom.getMemberByWs(ws);
  if (!member) return;

  if (teamRoom.state === 'playing') {
    if (teamRoom.battleMode === '1v1') {
      // 1v1 mode: opponent left = immediate win for the other player
      member.disconnectedAt = Date.now();
      member.ws = null;

      console.log(`[Server] Player ${member.playerId} disconnected from 1v1 game ${teamRoom.id}`);

      teamRoom.broadcast(NET_MSG.OPPONENT_LEFT, { playerId: member.playerId });
    } else {
      // 3v3 mode: mark as disconnected, allow reconnect with bot takeover
      member.disconnectedAt = Date.now();
      member.ws = null;

      console.log(`[Server] Player ${member.playerId} disconnected from team game ${teamRoom.id}`);

      teamRoom.broadcast(NET_MSG.PLAYER_DISCONNECT, { playerId: member.playerId });

      // Set bot takeover timer
      setTimeout(() => {
        // Only take over if still disconnected and game is still playing
        if (member.disconnectedAt && !member.ws && teamRoom.state === 'playing') {
          member.isBot = true;
          console.log(`[Server] Bot takeover for ${member.playerId} in team ${teamRoom.id}`);
          teamRoom.broadcast(NET_MSG.BOT_TAKEOVER, { playerId: member.playerId });
        }
      }, TEAM_RECONNECT_TIMEOUT);
    }
  } else if (teamRoom.state === 'finished') {
    // Game finished, keep player in room for potential rematch
    // Just mark as disconnected but don't remove from room
    member.disconnectedAt = Date.now();
    member.ws = null;
    console.log(`[Server] Player ${member.playerId} disconnected from finished game ${teamRoom.id} (kept for rematch)`);
  } else if (teamRoom.state === 'matching') {
    // During matching, cancel match and remove player
    if (teamRoom.leaderId === playerInfo.playerId) {
      cancelMatch(teamRoom);
    }
    handleLeaveTeam(ws, {});
  } else {
    // Not in game, just remove
    // For 1v1 waiting rooms, notify the other player
    if (teamRoom.battleMode === '1v1') {
      teamRoom.broadcast(NET_MSG.OPPONENT_LEFT, { playerId: member.playerId }, ws);
    }
    handleLeaveTeam(ws, {});
  }
}

function handleReconnect(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo) return;

  const { teamId, playerId } = data;
  if (!teamId || !playerId) return;

  const teamRoom = teamRooms.get(teamId);
  if (!teamRoom || teamRoom.state !== 'playing') {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Game not found or already ended' });
    return;
  }

  const member = teamRoom.getMember(playerId);
  if (!member || !member.disconnectedAt) {
    sendMessage(ws, NET_MSG.ROOM_ERROR, { message: 'Cannot reconnect' });
    return;
  }

  // Reconnect successful
  member.ws = ws;
  member.disconnectedAt = null;
  member.isBot = false;
  playerInfo.teamId = teamId;
  playerInfo.playerId = playerId;

  console.log(`[Server] Player ${playerId} reconnected to team ${teamId}`);

  // Send current game state
  sendMessage(ws, NET_MSG.RECONNECT_OK, {
    teamState: teamRoom.getTeamState(),
    mapId: teamRoom.mapId,
    teamABaseHp: teamRoom.teamABaseHp,
    teamBBaseHp: teamRoom.teamBBaseHp,
    elapsed: Math.floor((Date.now() - teamRoom.gameStartTime) / 1000),
  });

  teamRoom.broadcast(NET_MSG.TEAM_STATE, teamRoom.getTeamState());
}

/**
 * Handle a rematch request from a player.
 * When all human players in the room request rematch, reset and restart.
 */
function handleRematch(ws, data) {
  const playerInfo = players.get(ws);
  if (!playerInfo || !playerInfo.teamId) {
    console.log(`[Server] Rematch rejected: no playerInfo or teamId`, playerInfo ? playerInfo.playerId : 'unknown');
    return;
  }

  const teamRoom = teamRooms.get(playerInfo.teamId);
  if (!teamRoom || teamRoom.state !== 'finished') {
    console.log(`[Server] Rematch rejected: room not found or not finished`, playerInfo.teamId, teamRoom ? teamRoom.state : 'no room');
    return;
  }

  // Initialize rematch set if needed
  if (!teamRoom._rematchPlayers) {
    teamRoom._rematchPlayers = new Set();
  }

  // Update the member's ws reference (in case of reconnect)
  const member = teamRoom.getMember(playerInfo.playerId);
  if (member) {
    member.ws = ws;
    member.disconnectedAt = null;
  }

  teamRoom._rematchPlayers.add(playerInfo.playerId);

  // Count all human members (connected or not)
  const allHumans = teamRoom.getAllHumanMembers();
  console.log(`[Server] Rematch request from ${playerInfo.playerId} in room ${teamRoom.id} (${teamRoom._rematchPlayers.size}/${allHumans.length})`);

  // Notify all players about who wants rematch
  teamRoom.broadcast(NET_MSG.REMATCH_READY, {
    playerId: playerInfo.playerId,
    readyCount: teamRoom._rematchPlayers.size,
    totalCount: allHumans.length,
  });

  // Check if all human players who are still connected want rematch
  const connectedHumans = allHumans.filter(
    m => m.ws && m.ws.readyState === 1
  );
  const allReady = connectedHumans.length > 0 && connectedHumans.every(
    m => teamRoom._rematchPlayers.has(m.playerId)
  );

  console.log(`[Server] Rematch check: connectedHumans=${connectedHumans.length}, allReady=${allReady}, rematchPlayers=[${[...teamRoom._rematchPlayers].join(',')}]`);

  if (allReady) {
    // Clear the cleanup timer
    if (teamRoom._rematchTimer) {
      clearTimeout(teamRoom._rematchTimer);
      teamRoom._rematchTimer = null;
    }

    console.log(`[Server] All players ready for rematch in room ${teamRoom.id}`);

    // Reset room for rematch
    teamRoom.resetForRematch();

    // Remove any bot members from previous game (they'll be re-filled)
    teamRoom.teamA = teamRoom.teamA.filter(m => !m.isBot);
    teamRoom.teamB = teamRoom.teamB.filter(m => !m.isBot);

    // Fill with bots if needed
    if (teamRoom.fillWithBotsEnabled) {
      teamRoom.fillWithBots();
    }

    // Start the game again after a short delay
    if (teamRoom.battleMode === '1v1') {
      teamRoom.state = 'playing';
      setTimeout(() => {
        startTeamGame(teamRoom);
      }, 3500); // 3.5s countdown
    } else {
      teamRoom.state = 'playing';
      startTeamGame(teamRoom);
    }
  }
}

function cleanupTeamRoom(teamId) {
  const teamRoom = teamRooms.get(teamId);
  if (!teamRoom) return;

  if (teamRoom.matchTimer) clearTimeout(teamRoom.matchTimer);
  if (teamRoom._rematchTimer) clearTimeout(teamRoom._rematchTimer);

  // Clear teamId for all human members
  for (const member of teamRoom.getAllHumanMembers()) {
    if (member.ws) {
      const info = players.get(member.ws);
      if (info) info.teamId = null;
    }
  }

  // Remove from match pool
  const idx = teamMatchPool.indexOf(teamRoom);
  if (idx !== -1) teamMatchPool.splice(idx, 1);

  teamRooms.delete(teamId);
  console.log(`[Server] Team room ${teamId} cleaned up`);
}

// ============================================================
// Message Handlers
// ============================================================

function handleMessage(ws, rawData) {
  let msg;
  try {
    msg = JSON.parse(rawData);
  } catch (e) {
    console.error('[Server] Invalid JSON:', rawData);
    return;
  }

  const { type, data, playerId, nickname, avatarUrl, skinId } = msg;

  // Update player info
  const playerInfo = players.get(ws);
  if (playerInfo) {
    playerInfo.lastPing = Date.now();
    if (playerId && !playerInfo.playerId) {
      playerInfo.playerId = playerId;
    }
    // Refresh nickname on every message (it may be granted mid-session).
    if (typeof nickname === 'string' && nickname) {
      if (playerInfo.nickname !== nickname) {
        playerInfo.nickname = nickname;
        // Also propagate into any active team room member entry.
        if (playerInfo.teamId) {
          const tr = teamRooms.get(playerInfo.teamId);
          if (tr) {
            const member = tr.getMemberByWs(ws);
            if (member && member.nickname !== nickname) {
              member.nickname = nickname;
              // Broadcast regardless of room state (forming / matching / playing)
              // so that peers always render the latest display name — in 3v3 a
              // player may only tap the UserInfoButton AFTER the match starts.
              tr.broadcast(NET_MSG.TEAM_STATE, tr.getTeamState());
            }
          }
        }
      }
    }
    // Refresh avatarUrl on every message (it may be granted mid-session).
    if (typeof avatarUrl === 'string' && avatarUrl && playerInfo.avatarUrl !== avatarUrl) {
      playerInfo.avatarUrl = avatarUrl;
      if (playerInfo.teamId) {
        const tr = teamRooms.get(playerInfo.teamId);
        if (tr) {
          const member = tr.getMemberByWs(ws);
          if (member && member.avatarUrl !== avatarUrl) {
            member.avatarUrl = avatarUrl;
            tr.broadcast(NET_MSG.TEAM_STATE, tr.getTeamState());
          }
        }
      }
    }
    // Refresh skinId on every message (equipped skin may change mid-session).
    if (typeof skinId === 'string' && skinId && playerInfo.skinId !== skinId) {
      playerInfo.skinId = skinId;
      if (playerInfo.teamId) {
        const tr = teamRooms.get(playerInfo.teamId);
        if (tr) {
          const member = tr.getMemberByWs(ws);
          if (member && member.skinId !== skinId) {
            member.skinId = skinId;
            tr.broadcast(NET_MSG.TEAM_STATE, tr.getTeamState());
          }
        }
      }
    }
  }

  switch (type) {
    case NET_MSG.PING:
      sendMessage(ws, NET_MSG.PONG, {});
      break;

    // 1v1 PVP
    case NET_MSG.CREATE_ROOM:
      handleCreateRoom(ws, data || {});
      break;

    case NET_MSG.JOIN_ROOM:
      handleJoinRoom(ws, data || {});
      break;

    // Relay gameplay messages
    case NET_MSG.PLAYER_INPUT:
    // Messages where playerId is the sender themselves — override to prevent spoofing
    case NET_MSG.PLAYER_INPUT:
      if (playerInfo && playerInfo.teamId) {
        relayToTeamRoom(ws, type, { ...data, playerId: playerInfo.playerId });
      } else if (playerInfo && playerInfo.roomId) {
        relayToOpponent(ws, type, data || {});
      }
      break;

    // Messages where playerId/victimId/killerId refer to specific entities (bots, etc.)
    // — must NOT be overwritten with the sender's playerId
    case NET_MSG.PLAYER_STATE:
    case NET_MSG.BULLET_FIRE:
    case NET_MSG.BULLET_HIT:
    case NET_MSG.PLAYER_HIT:
    case NET_MSG.PLAYER_KILLED:
    case NET_MSG.GAME_OVER:
    case NET_MSG.TERRAIN_CHANGE:
    case NET_MSG.BOT_STATE:
      if (playerInfo && playerInfo.teamId) {
        relayToTeamRoom(ws, type, data || {});
      } else if (playerInfo && playerInfo.roomId) {
        relayToOpponent(ws, type, data || {});
      }
      break;

    // 3v3 Team messages
    case NET_MSG.CREATE_TEAM:
      handleCreateTeam(ws, data || {});
      break;

    case NET_MSG.JOIN_TEAM:
      handleJoinTeam(ws, data || {});
      break;

    case NET_MSG.LEAVE_TEAM:
      handleLeaveTeam(ws, data || {});
      break;

    case NET_MSG.TEAM_READY:
      handleTeamReady(ws, data || {});
      break;

    case NET_MSG.TEAM_KICK:
      handleTeamKick(ws, data || {});
      break;

    case NET_MSG.TEAM_DISBAND:
      handleTeamDisband(ws, data || {});
      break;

    case NET_MSG.MATCH_START:
      handleMatchStart(ws, data || {});
      break;

    case NET_MSG.MATCH_CANCEL:
      handleMatchCancel(ws, data || {});
      break;

    case NET_MSG.SOLO_MATCH:
      handleSoloMatch(ws, data || {});
      break;

    case NET_MSG.BASE_HIT:
      // All modes now use server-authoritative base HP tracking via TeamRoom
      if (playerInfo && playerInfo.teamId) {
        handleBaseHit(ws, data || {});
      } else if (playerInfo && playerInfo.roomId) {
        // Legacy fallback for old 1v1 rooms
        relayToOpponent(ws, type, data || {});
      }
      break;

    case NET_MSG.PLAYER_RESPAWN:
      if (playerInfo && playerInfo.teamId) {
        relayToTeamRoom(ws, type, { ...data, playerId: playerInfo.playerId });
      }
      break;

    case NET_MSG.RECONNECT:
      handleReconnect(ws, data || {});
      break;

    case NET_MSG.REMATCH:
      handleRematch(ws, data || {});
      break;

    default:
      console.warn(`[Server] Unknown message type: ${type}`);
  }
}

// ============================================================
// Anti-Cheat & Rate Limiting
// ============================================================

/** @type {Map<string, { count: number, resetTime: number }>} IP-based ad request tracking */
const adRequestTracker = new Map();

/** @type {Map<string, number[]>} IP-based purchase tracking for anomaly detection */
const purchaseTracker = new Map();

/**
 * Check if an IP has exceeded the ad request rate limit.
 * @param {string} ip
 * @returns {boolean} true if rate limited
 */
function isAdRateLimited(ip) {
  const now = Date.now();
  const tracker = adRequestTracker.get(ip);

  if (!tracker || now > tracker.resetTime) {
    adRequestTracker.set(ip, { count: 1, resetTime: now + 60000 }); // 1 minute window
    return false;
  }

  tracker.count++;
  // More than 20 ad requests per minute is suspicious
  if (tracker.count > 20) {
    console.warn(`[AntiCheat] Ad rate limit triggered for IP: ${ip} (${tracker.count} requests/min)`);
    return true;
  }
  return false;
}

/**
 * Check for suspicious purchase patterns.
 * @param {string} ip
 * @param {number} amount - Purchase amount in fen
 * @returns {{ suspicious: boolean, reason?: string }}
 */
function checkPurchaseAnomaly(ip, amount) {
  const now = Date.now();
  if (!purchaseTracker.has(ip)) {
    purchaseTracker.set(ip, []);
  }

  const history = purchaseTracker.get(ip);
  // Clean old entries (keep last 24 hours)
  const cutoff = now - 24 * 60 * 60 * 1000;
  while (history.length > 0 && history[0] < cutoff) {
    history.shift();
  }

  history.push(now);

  // Flag: more than 10 purchases in 24 hours
  if (history.length > 10) {
    console.warn(`[AntiCheat] Suspicious purchase frequency from IP: ${ip} (${history.length} in 24h)`);
    return { suspicious: true, reason: 'high_frequency' };
  }

  // Flag: single purchase > ¥500 (50000 fen)
  if (amount > 50000) {
    console.warn(`[AntiCheat] Large purchase flagged from IP: ${ip} (¥${amount / 100})`);
    return { suspicious: true, reason: 'large_amount' };
  }

  return { suspicious: false };
}

/**
 * Validate game session stats for impossible values.
 * @param {object} stats - { kills, timeElapsed, score, playerId }
 * @returns {{ valid: boolean, flags: string[] }}
 */
function validateGameStats(stats) {
  const flags = [];

  if (!stats) return { valid: true, flags };

  // Impossible kill rate: >10 kills per minute
  if (stats.kills && stats.timeElapsed) {
    const killsPerMinute = stats.kills / (stats.timeElapsed / 60);
    if (killsPerMinute > 10) {
      flags.push('impossible_kill_rate');
    }
  }

  // Impossible score
  if (stats.score && stats.score > 100000) {
    flags.push('impossible_score');
  }

  // Impossible speed (if movement data is provided)
  if (stats.maxSpeed && stats.maxSpeed > 500) {
    flags.push('impossible_speed');
  }

  if (flags.length > 0) {
    console.warn(`[AntiCheat] Suspicious game stats from ${stats.playerId || 'unknown'}: ${flags.join(', ')}`);
  }

  return { valid: flags.length === 0, flags };
}

// Clean up rate limit trackers periodically
setInterval(() => {
  const now = Date.now();
  for (const [ip, tracker] of adRequestTracker) {
    if (now > tracker.resetTime) {
      adRequestTracker.delete(ip);
    }
  }
  // Clean purchase tracker (entries older than 24h)
  for (const [ip, history] of purchaseTracker) {
    const cutoff = now - 24 * 60 * 60 * 1000;
    while (history.length > 0 && history[0] < cutoff) {
      history.shift();
    }
    if (history.length === 0) {
      purchaseTracker.delete(ip);
    }
  }
}, 300000); // Every 5 minutes

// ============================================================
// ============================================================
// WebSocket Server (noServer mode, shares HTTP server with express)
// ============================================================
// Use noServer mode so the WS upgrade only fires on the configured path.
const wss = new WebSocketServer({ noServer: true });

server.on('upgrade', (req, socket, head) => {
  // Only upgrade on the configured WebSocket path; reject any other path.
  const pathname = (req.url || '').split('?')[0];
  if (pathname !== WS_PATH) {
    console.warn(`[Server] Rejected WebSocket upgrade on unexpected path: ${req.url}`);
    socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
    socket.destroy();
    return;
  }
  wss.handleUpgrade(req, socket, head, (ws) => {
    wss.emit('connection', ws, req);
  });
});

wss.on('connection', (ws, req) => {
  const ip = req.socket.remoteAddress;
  console.log(`[Server] New connection from ${ip}`);

  const playerInfo = new PlayerInfo(ws, null);
  players.set(ws, playerInfo);

  ws.on('message', (rawData) => {
    handleMessage(ws, rawData.toString());
  });

  ws.on('close', () => {
    console.log(`[Server] Connection closed from ${ip}`);
    handleTeamPlayerDisconnect(ws);
    leaveRoom(ws);
    players.delete(ws);
  });

  ws.on('error', (err) => {
    console.error(`[Server] WebSocket error from ${ip}:`, err.message);
  });
});

// ============================================================
// Heartbeat & Cleanup
// ============================================================
setInterval(() => {
  const now = Date.now();

  // Clean up stale players
  for (const [ws, info] of players) {
    if (now - info.lastPing > HEARTBEAT_INTERVAL * 3) {
      console.log(`[Server] Removing stale player ${info.playerId}`);
      leaveRoom(ws);
      players.delete(ws);
      try { ws.terminate(); } catch (e) { /* ignore */ }
    }
  }

  // Clean up old empty rooms
  for (const [id, room] of rooms) {
    if (room.isEmpty() || now - room.createdAt > ROOM_TIMEOUT) {
      rooms.delete(id);
      console.log(`[Server] Room ${id} cleaned up`);
    }
  }

  // Clean up old team rooms
  for (const [id, teamRoom] of teamRooms) {
    const allHumans = teamRoom.getAllHumanMembers();
    const hasConnected = allHumans.some(m => m.ws && m.ws.readyState === 1);
    if (!hasConnected || now - teamRoom.createdAt > ROOM_TIMEOUT * 2) {
      cleanupTeamRoom(id);
      console.log(`[Server] Team room ${id} cleaned up (stale)`);
    }
  }
}, HEARTBEAT_INTERVAL);

// ============================================================
// Startup
// ============================================================
server.listen(PORT, HOST, () => {
  console.log(`[Tank War Server] Running on ${HOST}:${PORT}`);
  console.log(`[Tank War Server] WebSocket path: ${WS_PATH}`);
  console.log(`[Tank War Server] Health check paths: /health, /tankwar/health`);
  console.log(`[Tank War Server] HTTP API URL: http://${HOST}:${PORT}`);
});
