// /server_modules/gameManager.js
const { v4: uuidv4 } = require('uuid'); // Убедитесь, что uuidv4 установлен: npm install uuid
const GameInstance = require('./gameInstance'); // Убедитесь, что GameInstance экспортируется из gameInstance.js
const gameData = require('./data'); // Нужен для getAvailablePvPGamesListForClient и данных персонажей
const GAME_CONFIG = require('./config'); // Нужен для GAME_CONFIG.PLAYER_ID и других констант

class GameManager {
    constructor(io) {
        this.io = io; // Ссылка на Socket.IO сервер для широковещательных рассылок
        this.games = {}; // { gameId: GameInstance } - Все активные или ожидающие игры
        this.userIdentifierToGameId = {}; // { userId|socketId: gameId } - Какому пользователю какая игра соответствует (более стабильно, чем socket.id)
        this.pendingPvPGames = []; // [gameId] - ID PvP игр, ожидающих второго игрока

        // Навешиваем обработчик события 'gameOver' на Socket.IO сервер
        // Это событие исходит от экземпляра GameInstance при завершении игры (по HP или дисконнекту)
        // Мы слушаем его здесь, чтобы GameManager мог очистить ссылки.
        // Примечание: Это событие отправляется всем в комнате игры. GameManager слушает его через io.sockets.sockets.on,
        // но удобнее слушать его на уровне io, если возможно, или добавить специальный emit из GameInstance.
        // Текущая архитектура (GameInstance напрямую вызывает io.to(...).emit('gameOver', ...)) уже рабочая.
        // GameManager сам должен отреагировать на завершение, проверяя gameState.isGameOver после каждого действия/хода.
        // Или GameInstance должен вызвать специальный метод GameManager при gameOver.
        // Давайте сделаем GameInstance вызывать метод GameManager при gameOver.
    }

    /**
     * Удаляет предыдущую ожидающую игру пользователя, если таковая существует.
     * Это предотвращает создание множества пустых игр одним пользователем.
     * @param {string} currentSocketId - ID текущего сокета.
     * @param {string|number} identifier - userId или socketId пользователя.
     * @param {string|null} excludeGameId - ID игры, которую НЕ нужно удалять (например, если пользователь присоединяется к своей же игре).
     */
    _removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
        // Ищем игру по идентификатору пользователя
        const oldPendingGameId = this.userIdentifierToGameId[identifier];

        // Проверяем, что нашли игру, она не исключена, и она все еще существует в списке игр
        if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
            const gameToRemove = this.games[oldPendingGameId];
            // Проверяем, что игра является ожидающей PvP игрой с одним игроком
            if (gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
                // Проверяем, что этот пользователь является владельцем этой ожидающей игры
                // Владелец в pendingPvPGames - это всегда тот, кто ее создал (первый игрок в слоте PLAYER_ID)
                const oldOwnerInfo = Object.values(gameToRemove.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);

                // Проверяем, что владелец игры существует и его идентификатор совпадает
                if (oldOwnerInfo && (oldOwnerInfo.identifier === identifier)) {
                    console.log(`[GameManager] Пользователь ${identifier} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`);

                    // Используем централизованную функцию очистки
                    this._cleanupGame(oldPendingGameId, 'replaced_by_new_game');

                    // Оповещаем клиентов об обновленном списке игр (уже внутри _cleanupGame)
                    // this.broadcastAvailablePvPGames();
                }
            } else {
                // Если игра не соответствует критериям ожидающей игры, но идентификатор был связан с ней,
                // это может означать, что игра уже началась или была завершена.
                // Просто очищаем ссылку, если она не ведет в исключенную игру.
                // Идентификатор должен был быть очищен из userIdentifierToGameId при старте или завершении игры.
                // На всякий случай убеждаемся, что мы не удаляем ссылку на игру, к которой только что присоединились.
                if (this.userIdentifierToGameId[identifier] !== excludeGameId) {
                    console.warn(`[GameManager] Удаление потенциально некорректной ссылки userIdentifierToGameId[${identifier}] на игру ${oldPendingGameId}.`);
                    delete this.userIdentifierToGameId[identifier];
                }
            }
        }
        // Если oldPendingGameId не найдена, или она равна excludeGameId, ничего не делаем.
    }


    /**
     * Создает новую игру.
     * @param {object} socket - Сокет игрока, создающего игру.
     * @param {string} [mode='ai'] - Режим игры ('ai' или 'pvp').
     * @param {string} [chosenCharacterKey='elena'] - Выбранный персонаж для первого игрока в PvP.
     * @param {string|number} identifier - ID пользователя (userId или socketId).
     */
    createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) {
        // Удаляем старые ожидающие игры этого пользователя, прежде чем создавать новую
        this._removePreviousPendingGames(socket.id, identifier);

        // Проверяем, не находится ли пользователь уже в какой-то игре (активной или ожидающей)
        // Проверяем наличие ссылки на игру по идентификатору пользователя
        if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
            console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на создание.`);
            socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
            // Можно попробовать отправить состояние текущей игры пользователю
            this.handleRequestGameState(socket, identifier);
            return;
        }


        const gameId = uuidv4(); // Генерируем уникальный ID для игры
        // Передаем ссылку на GameManager в GameInstance, чтобы он мог вызвать _notifyGameEnded
        const game = new GameInstance(gameId, this.io, mode, this); // <-- ПЕРЕДАЕМ GameManager
        game.ownerIdentifier = identifier; // Сохраняем идентификатор создателя
        this.games[gameId] = game; // Добавляем игру в список активных игр

        // В AI режиме игрок всегда Елена, в PvP - тот, кого выбрали при создании
        const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';

        // Добавляем игрока в созданный экземпляр игры, передавая идентификатор
        // GameInstance.addPlayer принимает socket, chosenCharacterKey, identifier
        if (game.addPlayer(socket, charKeyForInstance, identifier)) {
            this.userIdentifierToGameId[identifier] = gameId; // Связываем идентификатор пользователя с этой игрой
            console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${identifier} (сокет: ${socket.id}, выбран: ${charKeyForInstance})`);

            // Уведомляем игрока, что игра создана, и передаем его технический ID слота
            const assignedPlayerId = game.players[socket.id]?.id; // ID слота все еще берем из playerInfo по socket.id
            if (!assignedPlayerId) {
                // Если по какой-то причине не удалось назначить ID игрока, удаляем игру и отправляем ошибку
                // Используем централизованную функцию очистки
                this._cleanupGame(gameId, 'player_add_failed');
                console.error(`[GameManager] Ошибка при создании игры ${gameId}: Не удалось назначить ID игрока сокету ${socket.id} (идентификатор ${identifier}).`);
                socket.emit('gameError', { message: 'Ошибка сервера при создании игры.' });
                return;
            }
            socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });


            // --- Логика старта игры ---
            // Если игра AI и теперь с 1 игроком, или PvP и теперь с 2 игроками, запускаем ее немедленно
            if ((game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2)) {
                console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`);
                // Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
                const isInitialized = game.initializeGame();
                if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
                    game.startGame(); // Запускаем игру
                } else {
                    console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`);
                    // initializeGame уже должен был добавить ошибку в лог игры и отправить gameError клиентам
                    // Возможно, стоит вызвать cleanupGame здесь при ошибке инициализации
                    this._cleanupGame(gameId, 'initialization_failed');
                }

                // Если игра PvP и только что заполнилась, удаляем ее из списка ожидающих
                // Идентификаторы игроков остаются связанными с игрой в userIdentifierToGameId до ее завершения.
                if (game.mode === 'pvp' && game.playerCount === 2) {
                    const gameIndex = this.pendingPvPGames.indexOf(gameId);
                    if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
                    // Связи userIdentifierToGameId[identifier] НЕ УДАЛЯЕМ! Они нужны для активной игры.
                    this.broadcastAvailablePvPGames(); // Обновляем список у всех клиентов
                }


            } else if (mode === 'pvp' && game.playerCount === 1) {
                // Если игра PvP и ожидает второго игрока, добавляем ее в список ожидающих
                if (!this.pendingPvPGames.includes(gameId)) {
                    this.pendingPvPGames.push(gameId); // Добавляем ID игры в список ожидающих
                }
                // userIdentifierToGameId для создателя уже установлен выше

                // Частичная инициализация gameState для отображения Player 1 на UI ожидания
                // initializeGame вызывается при playerCount === 1 в GameInstance
                game.initializeGame();

                this.broadcastAvailablePvPGames(); // Обновляем список у всех
            }
            // --- КОНЕЦ Логики старта игры ---


        } else {
            // Если не удалось добавить игрока в GameInstance (например, уже 2 игрока - хотя проверили выше), удаляем игру
            // Используем централизованную функцию очистки
            this._cleanupGame(gameId, 'player_add_failed');
            // GameInstance.addPlayer уже отправил ошибку клиенту
            console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}. Игра удалена.`);
        }
    }

    /**
     * Присоединяет игрока к существующей игре по ID.
     * @param {object} socket - Сокет игрока.
     * @param {string} gameId - ID игры, к которой нужно присоединиться.
     * @param {string|number} identifier - ID пользователя (userId).
     */
    joinGame(socket, gameId, identifier) { // В joinGame всегда передается userId, т.к. PvP требует логина
        const game = this.games[gameId]; // Находим игру по ID

        // Проверки перед присоединением
        if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
        if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться как к PvP.' }); return; }
        if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; }
        // Проверка, не находится ли пользователь уже в какой-то игре
        if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]] && this.userIdentifierToGameId[identifier] !== gameId) {
            console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на присоединение.`);
            socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
            this.handleRequestGameState(socket, identifier); // Попробуем отправить состояние текущей игры
            return;
        }
        if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре.' }); return;} // Проверка на повторное присоединение по текущему сокету (хотя userIdentifierToGameId должен это предотвратить)

        // Удаляем старые ожидающие игры этого пользователя, исключая текущую игру, к которой присоединяемся
        this._removePreviousPendingGames(socket.id, identifier, gameId);

        // addPlayer в GameInstance сам определит персонажа для второго игрока на основе первого
        // GameInstance.addPlayer принимает socket, chosenCharacterKey (null для присоединения), identifier
        if (game.addPlayer(socket, null, identifier)) { // chosenCharacterKey для присоединяющегося игрока не нужен, передаем null
            this.userIdentifierToGameId[identifier] = gameId; // Связываем идентификатор пользователя с этой игрой
            console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) присоединился к PvP игре ${gameId}`);

            // --- Логика старта игры ---
            // Если игра PvP и теперь с 2 игроками, запускаем ее немедленно
            if (game.mode === 'pvp' && game.playerCount === 2) {
                console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`);
                // Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
                const isInitialized = game.initializeGame();
                if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
                    game.startGame(); // Запускаем игру
                } else {
                    console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`);
                    // initializeGame уже должен был добавить ошибку в лог игры и отправить gameError клиентам
                    // Возможно, стоит вызвать cleanupGame здесь при ошибке инициализации
                    this._cleanupGame(gameId, 'initialization_failed');
                }

                // Если игра PvP и только что заполнилась, удаляем ее из списка ожидающих
                const gameIndex = this.pendingPvPGames.indexOf(gameId);
                if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);

                // Связи userIdentifierToGameId[identifier] НЕ УДАЛЯЕМ! Они нужны для активной игры.
                // ownerIdentifier игры (идентификатор создателя) также остается.

                this.broadcastAvailablePvPGames(); // Обновляем список у всех клиентов
            }
            // --- КОНЕЦ Логики старта игры ---


        } else {
            // Сообщение об ошибке отправляется из game.addPlayer
            console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}.`);
        }
    }

    /**
     * Ищет случайную ожидающую PvP игру и присоединяет игрока к ней.
     * Если подходящих игр нет, создает новую ожидающую игру.
     * @param {object} socket - Сокет игрока.
     * @param {string} [chosenCharacterKeyForCreation='elena'] - Выбранный персонаж, если придется создавать новую игру.
     * @param {string|number} identifier - ID пользователя (userId).
     */
    findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) { // В findRandomGame всегда передается userId
        // Удаляем старые ожидающие игры этого пользователя
        this._removePreviousPendingGames(socket.id, identifier);

        // Проверяем, не находится ли пользователь уже в какой-то игре
        if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
            console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на поиск.`);
            socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
            this.handleRequestGameState(socket, identifier); // Попробуем отправить состояние текущей игры
            return;
        }

        let gameIdToJoin = null;
        // Персонаж, которого мы бы хотели видеть у оппонента (зеркальный нашему выбору для создания)
        const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena';

        // Ищем свободную игру в списке ожидающих
        for (const id of this.pendingPvPGames) {
            const pendingGame = this.games[id];
            // Проверяем, что игра существует, PvP, в ней только 1 игрок и это НЕ игра, которую создал сам текущий пользователь
            // Игрок не должен присоединяться к игре, которую создал сам.
            if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier) {
                // Нашли потенциальную игру. Проверяем предпочтительного оппонента.
                const firstPlayerInfo = Object.values(pendingGame.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); // В ожидающей игре всегда 1 игрок, он и есть players[0]
                if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) {
                    gameIdToJoin = id; // Нашли игру с предпочтительным оппонентом
                    break; // Выходим из цикла, т.к. нашли лучший вариант
                }
                // Если предпочтительного не нашли в этом цикле, сохраняем ID первой попавшейся (не своей) игры
                if (!gameIdToJoin) gameIdToJoin = id; // Сохраняем, но продолжаем искать предпочтительную
            }
        }

        if (gameIdToJoin) {
            // Присоединяемся к найденной игре. GameInstance.addPlayer сам назначит нужного персонажа второму игроку.
            console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) нашел игру ${gameIdToJoin} и присоединяется.`);
            this.joinGame(socket, gameIdToJoin, identifier); // Используем joinGame, т.к. логика присоединения одинакова
        } else {
            // Если свободных игр нет, создаем новую с выбранным персонажем
            console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) не нашел свободных игр. Создает новую.`);
            this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier); // Используем createGame
            // Клиент получит 'gameCreated', а 'noPendingGamesFound' используется для информационного сообщения
            // userIdentifierToGameId уже обновлен в createGame
            socket.emit('noPendingGamesFound', {
                message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.',
                gameId: this.userIdentifierToGameId[identifier], // ID только что созданной игры
                yourPlayerId: GAME_CONFIG.PLAYER_ID // При создании всегда PLAYER_ID
            });
        }
    }

    /**
     * Перенаправляет действие игрока соответствующему экземпляру игры.
     * @param {string|number} identifier - ID пользователя (userId или socketId).
     * @param {object} actionData - Данные о действии.
     */
    handlePlayerAction(identifier, actionData) { // Теперь принимаем identifier
        const gameId = this.userIdentifierToGameId[identifier]; // Находим ID игры по идентификатору пользователя
        const game = this.games[gameId]; // Находим экземпляр игры

        if (game && game.players) {
            // Находим текущий сокет ID пользователя в списке игроков этой игры
            const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
            const currentSocketId = playerInfo?.socket?.id;

            if (playerInfo && currentSocketId) {
                // Проверяем, что сокет с этим ID еще подключен.
                // Это дополнительная проверка, чтобы не обрабатывать действия от "зомби"-сокетов
                const actualSocket = this.io.sockets.sockets.get(currentSocketId);

                if (actualSocket && actualSocket.connected) {
                    // Передаем действие экземпляру игры, используя ТЕКУЩИЙ Socket ID
                    game.processPlayerAction(currentSocketId, actionData); // processPlayerAction в GameInstance использует socketId
                } else {
                    // Если сокет не найден или не подключен, это может быть старое действие от отключившегося сокета
                    console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его текущий сокет (${currentSocketId}) не найден или отключен.`);
                    // Не отправляем ошибку клиенту, так как он, вероятно, уже отключен или переподключается
                    // Клиент получит gameNotFound при следующем запросе состояния или gameError, если игра еще активна
                }
            } else {
                // Игрок не найден в списке players этой игры по идентификатору
                console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}) для игры ${gameId}, но его запись не найдена в game.players.`);
                // В таком случае, возможно, состояние userIdentifierToGameId некорректно.
                // Удаляем некорректную ссылку.
                delete this.userIdentifierToGameId[identifier];
                // Оповещаем клиента, что игра не найдена (он должен будет запросить состояние)
                const playerSocket = this.io.sockets.sockets.get(identifier); // Попробуем найти сокет по идентификатору (если он был socket.id)
                if (!playerSocket && playerInfo?.socket) { // Если не нашли по identifier, попробуем по сокету из playerInfo
                    playerSocket = playerInfo.socket;
                }
                if (playerSocket) {
                    playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' });
                }
            }
        } else {
            // Если игра не найдена по userIdentifierToGameId[identifier]
            console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его игра (ID: ${gameId}) не найдена в GameManager.`);
            // Удаляем некорректную ссылку
            delete this.userIdentifierToGameId[identifier];
            // Отправляем gameNotFound клиенту, если можем его найти (по identifier, если это socket.id)
            const playerSocket = this.io.sockets.sockets.get(identifier);
            if (playerSocket) {
                playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' });
            }
        }
    }

    /**
     * Обрабатывает отключение сокета игрока.
     * Вызывается из bc.js при событии 'disconnect'.
     * @param {string} socketId - ID отключившегося сокета.
     * @param {string|number} identifier - ID пользователя (userId или socketId).
     */
    handleDisconnect(socketId, identifier) { // Принимаем и socketId, и identifier
        // Ищем игру по идентификатору пользователя (более надежный способ после переподключения)
        const gameId = this.userIdentifierToGameId[identifier];
        const game = this.games[gameId];

        // Если игра найдена и в ней есть игрок с этим идентификатором (или сокетом)
        if (game && game.players) {
            // Находим информацию об игроке по идентификатору
            const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);

            if (playerInfo) {
                console.log(`[GameManager] Игрок ${identifier} (сокет: ${socketId}) отключился. В игре ${gameId}.`);

                // Удаляем игрока из экземпляра игры, передавая Socket ID, который отключился
                // GameInstance.removePlayer принимает socketId
                game.removePlayer(socketId); // Передаем socketId для удаления конкретного сокета

                // После удаления игрока из GameInstance, проверяем состояние игры и GameManager
                if (game.playerCount === 0) {
                    // Если в игре больше нет игроков, удаляем ее из GameManager
                    console.log(`[GameManager] Игра ${gameId} пуста после дисконнекта ${socketId} (идентификатор ${identifier}). Удаляем.`);
                    // Используем централизованную функцию очистки
                    this._cleanupGame(gameId, 'player_count_zero_on_disconnect');

                } else if (game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
                    // Если игра PvP, остался 1 игрок, и она еще не окончена (из-за дисконнекта),
                    // возвращаем ее в список ожидающих.
                    console.log(`[GameManager] Игра ${gameId} (PvP) теперь с 1 игроком после дисконнекта ${socketId} (идентификатор ${identifier}). Возвращаем в список ожидания.`);
                    if (!this.pendingPvPGames.includes(gameId)) {
                        this.pendingPvPGames.push(gameId);
                    }
                    // Удаляем ссылку на игру только для отключившегося идентификатора
                    delete this.userIdentifierToGameId[identifier];

                    // ownerIdentifier игры (если был userId) останется тем же, даже если отключился владелец.
                    // Это OK, ownerIdentifier используется для _removePreviousPendingGames.

                    this.broadcastAvailablePvPGames(); // Обновляем список у всех
                } else if (game.gameState?.isGameOver) {
                    // Если игра была окончена (например, дисконнект приводил к gameOver),
                    // просто удаляем ссылку на игру для отключившегося идентификатора.
                    console.log(`[GameManager] Игрок ${identifier} отключился из завершенной игры ${gameId}. Удаляем ссылку.`);
                    delete this.userIdentifierToGameId[identifier];
                } else {
                    // Игра не пуста и не вернулась в ожидание (например, AI игра, где остался игрок,
                    // или PvP игра с 2 игроками, где один отключился, а второй остался)
                    // Ссылка userIdentifierToGameId[identifier] для отключившегося игрока должна быть удалена.
                    console.log(`[GameManager] Игрок ${identifier} отключился из активной игры ${gameId} (mode: ${game.mode}, players: ${game.playerCount}). Удаляем ссылку.`);
                    delete this.userIdentifierToGameId[identifier];
                }
            } else {
                // Игра найдена, но игрока с этим идентификатором или сокетом в game.players нет.
                // Это может означать, что сокет отключился, но запись игрока была удалена раньше,
                // или identifier некорректен.
                console.warn(`[GameManager] Игрок с идентификатором ${identifier} (сокет: ${socketId}) не найден в game.players для игры ${gameId}.`);
                // Удаляем ссылку на игру для этого идентификатора, если она есть.
                delete this.userIdentifierToGameId[identifier];
                // Проверяем, возможно, этот сокет был в другой игре по старой ссылке socketToGame (удалено),
                // или это просто отключившийся сокет без активной игры.
            }
        } else {
            // Если игра не найдена по userIdentifierToGameId[identifier]
            console.log(`[GameManager] Отключился сокет ${socketId} (идентификатор ${identifier}). Игровая сессия по этому идентификатору не найдена.`);
            // Убеждаемся, что ссылка userIdentifierToGameId[identifier] удалена
            delete this.userIdentifierToGameId[identifier];
        }
    }


    /**
     * Централизованная функция для очистки игры после ее завершения.
     * Удаляет экземпляр игры и все связанные с ней ссылки.
     * Вызывается из GameInstance при gameOver (по HP или дисконнекту).
     * @param {string} gameId - ID завершенной игры.
     * @param {string} reason - Причина завершения (для логирования).
     * @returns {boolean} true, если игра найдена и очищена, иначе false.
     */
    _cleanupGame(gameId, reason = 'unknown_reason') { // <-- НОВЫЙ ПРИВАТНЫЙ МЕТОД
        const game = this.games[gameId];
        if (!game) {
            console.warn(`[GameManager] _cleanupGame called for unknown game ID: ${gameId}`);
            return false;
        }

        console.log(`[GameManager] Cleaning up game ${gameId} (Mode: ${game.mode}, Reason: ${reason})...`);

        // Удаляем ссылку userIdentifierToGameId для всех игроков, которые были в этой игре
        // Перебираем players в GameInstance, чтобы получить идентификаторы
        Object.values(game.players).forEach(playerInfo => {
            if (playerInfo && playerInfo.identifier && this.userIdentifierToGameId[playerInfo.identifier] === gameId) {
                delete this.userIdentifierToGameId[playerInfo.identifier];
                console.log(`[GameManager] Removed userIdentifierToGameId for ${playerInfo.identifier}.`);
            } else if (playerInfo && playerInfo.identifier) {
                console.warn(`[GameManager] User ${playerInfo.identifier} in game ${gameId} has incorrect userIdentifierToGameId reference.`);
                // Если ссылка некорректна, ничего не удаляем.
            }
        });

        // Удаляем ID игры из списка ожидающих, если она там была
        const pendingIndex = this.pendingPvPGames.indexOf(gameId);
        if (pendingIndex > -1) {
            this.pendingPvPGames.splice(pendingIndex, 1);
            console.log(`[GameManager] Removed game ${gameId} from pendingPvPGames.`);
        }

        // Удаляем сам экземпляр игры
        delete this.games[gameId];
        console.log(`[GameManager] Deleted GameInstance for game ${gameId}.`);

        // Оповещаем клиентов об обновленном списке игр (может понадобиться, если удалена ожидающая игра)
        // Или если активная игра была удалена, и игроки вернутся в лобби.
        this.broadcastAvailablePvPGames();

        return true;
    }


    /**
     * Формирует список доступных для присоединения PvP игр для клиента.
     * @returns {Array<object>} Массив объектов с информацией об играх.
     */
    getAvailablePvPGamesListForClient() {
        return this.pendingPvPGames
            .map(gameId => {
                const game = this.games[gameId];
                // Проверяем, что игра существует, это PvP, в ней 1 игрок, и она не окончена
                // gameState.isGameOver проверяется, чтобы исключить игры, которые могли завершиться сразу (очень маловероятно)
                if (game && game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) {
                    let firstPlayerUsername = 'Игрок';
                    let firstPlayerCharacterName = '';

                    // Находим информацию о первом игроке (он всегда в слоте GAME_CONFIG.PLAYER_ID в ожидающей игре)
                    const firstPlayerInfo = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);

                    if (firstPlayerInfo) {
                        // Получаем имя пользователя из userData, если залогинен
                        if (firstPlayerInfo.socket?.userData?.username) {
                            firstPlayerUsername = firstPlayerInfo.socket.userData.username;
                        } else {
                            // Если нет userData.username, используем часть identifier
                            firstPlayerUsername = `User#${String(firstPlayerInfo.identifier).substring(0,6)}`; // Приводим identifier к строке
                        }

                        // Получаем имя персонажа из chosenCharacterKey
                        const charKey = firstPlayerInfo.chosenCharacterKey;
                        if (charKey) {
                            // Используем _getCharacterBaseData напрямую, т.к. gameData доступен
                            const charBaseStats = this._getCharacterBaseData(charKey);
                            if (charBaseStats && charBaseStats.name) {
                                firstPlayerCharacterName = charBaseStats.name;
                            } else {
                                //console.warn(`[GameManager] getAvailablePvPGamesList: Не удалось найти имя для charKey '${charKey}' в gameData.`);
                                firstPlayerCharacterName = charKey; // В крайнем случае используем ключ
                            }
                        } else {
                            //console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo.chosenCharacterKey отсутствует для игры ${gameId}.`);
                        }
                    } else {
                        console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo (Player 1) не найдена для ожидающей игры ${gameId}.`);
                        firstPlayerUsername = 'Неизвестный игрок'; // Если даже игрока не нашли в players
                    }

                    // Формируем строку статуса для отображения в списке
                    let statusString = `Ожидает 1 игрока (Создал: ${firstPlayerUsername}`;
                    if (firstPlayerCharacterName) {
                        statusString += ` за ${firstPlayerCharacterName}`;
                    }
                    statusString += `)`;

                    return {
                        id: gameId, // Отправляем полный ID, но в списке UI показываем обрезанный
                        status: statusString
                    };
                }
                // Если игра не соответствует критериям ожидающей (например, пуста, заполнена, окончена), не включаем ее
                if (game && !this.pendingPvPGames.includes(gameId)) {
                    // Если игра есть, но не в pendingPvPGames, она не должна тут обрабатываться.
                } else if (game && game.playerCount === 1 && (game.gameState?.isGameOver || !game.gameState)) {
                    // Игра с 1 игроком, но окончена или не инициализирована - не показывать
                } else if (game && game.playerCount === 2) {
                    // Игра заполнена - не показывать
                } else if (game && game.playerCount === 0) {
                    // Игра пуста - ее надо было удалить при дисконнекте последнего игрока.
                    // Возможно, тут нужна очистка таких "потерянных" игр.
                    console.warn(`[GameManager] getAvailablePvPGamesList: Найдена пустая игра ${gameId} в games. Удаляем.`);
                    delete this.games[gameId]; // Удаляем потерянную игру
                    // Очистка из pendingPvPGames не нужна, т.к. она удаляется при playerCount === 0
                }

                return null; // Исключаем игры, не соответствующие критериям или удаленные
            })
            .filter(info => info !== null); // Удаляем null из результатов map
    }

    /**
     * Отправляет обновленный список доступных PvP игр всем подключенным клиентам.
     */
    broadcastAvailablePvPGames() {
        const availableGames = this.getAvailablePvPGamesListForClient();
        this.io.emit('availablePvPGamesList', availableGames);
        console.log(`[GameManager] Обновлен список доступных PvP игр. Всего: ${availableGames.length}`);
    }

    /**
     * Получает список активных игр для отладки на сервере.
     * @returns {Array<object>} Список объектов с краткой информацией об играх.
     */
    getActiveGamesList() { // Для отладки на сервере
        return Object.values(this.games).map(game => {
            // Получаем имена персонажей из gameState, если игра инициализирована, иначе из chosenCharacterKey/default
            let playerSlotCharName = game.gameState?.player?.name || (game.playerCharacterKey ? this._getCharacterBaseData(game.playerCharacterKey)?.name : 'N/A (ожидание)');
            let opponentSlotCharName = game.gameState?.opponent?.name || (game.opponentCharacterKey ? this._getCharacterBaseData(game.opponentCharacterKey)?.name : 'N/A (ожидание)');

            // Проверяем наличие игроков в слотах, чтобы уточнить статус
            const playerInSlot1 = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
            const playerInSlot2 = Object.values(game.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);

            if (!playerInSlot1) playerSlotCharName = 'Пусто';
            if (!playerInSlot2 && game.mode === 'pvp') opponentSlotCharName = 'Ожидание...'; // В PvP слоты могут быть пустыми
            if (!playerInSlot2 && game.mode === 'ai' && game.aiOpponent) opponentSlotCharName = 'Балард (AI)'; // В AI слоте оппонента всегда AI

            return {
                id: game.id.substring(0,8), // Обрезанный ID для удобства
                mode: game.mode,
                playerCount: game.playerCount,
                isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A (Не инициализирована)',
                playerSlot: playerSlotCharName,
                opponentSlot: opponentSlotCharName,
                ownerIdentifier: game.ownerIdentifier || 'N/A',
                pending: this.pendingPvPGames.includes(game.id),
                turn: game.gameState ? `Ход ${game.gameState.turnNumber}, ${game.gameState.isPlayerTurn ? (playerInSlot1?.identifier || 'Player Slot') : (playerInSlot2?.identifier || 'Opponent Slot')}` : 'N/A'
            };
        });
    }

    /**
     * Обрабатывает запрос клиента на gameState (например, при переподключении).
     * Находит игру пользователя по его идентификатору и отправляет ему актуальное состояние.
     * Также обновляет ссылку на сокет в GameInstance.
     * @param {object} socket - Сокет клиента, запросившего состояние.
     * @param {string|number} identifier - ID пользователя (userId или socketId).
     */
    handleRequestGameState(socket, identifier) { // Принимаем socket и identifier
        // Ищем игру пользователя по его идентификатору
        const gameId = this.userIdentifierToGameId[identifier];
        let game = null;

        if (gameId) {
            game = this.games[gameId];
        }

        // Если игра найдена и она существует, и в ней есть игрок с этим идентификатором
        if (game && game.players) {
            const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);

            if (playerInfo) {
                // Проверяем, если игра окончена, не восстанавливаем состояние, а информируем
                if (game.gameState?.isGameOver) {
                    console.log(`[GameManager] Reconnected user ${identifier} to game ${gameId} which is already over. Sending gameNotFound.`);
                    // Удаляем ссылку на оконченную игру для этого пользователя
                    delete this.userIdentifierToGameId[identifier];
                    // Отправляем gameNotFound, чтобы клиент вернулся в меню
                    socket.emit('gameNotFound', { message: 'Ваша предыдущая игровая сессия уже завершена.' });
                    return; // Прекращаем обработку
                }


                console.log(`[GameManager] Found game ${gameId} for identifier ${identifier} (role ${playerInfo.id}). Reconnecting socket ${socket.id}.`);

                // --- Обновляем GameInstance: заменяем старый сокет на новый для этого игрока ---
                // Удаляем старую запись игрока по старому socket.id, если она есть и отличается
                const oldSocketId = playerInfo.socket?.id;
                if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) {
                    console.log(`[GameManager] Updating socket ID for player ${identifier} from ${oldSocketId} to ${socket.id} in game ${gameId}.`);
                    delete game.players[oldSocketId]; // Удаляем запись по старому socketId
                    // playerCount не уменьшаем/увеличиваем, т.к. это тот же игрок, просто сменил сокет
                    // Удаляем ссылку на старый сокет по роли
                    if (game.playerSockets[playerInfo.id]?.id === oldSocketId) {
                        delete game.playerSockets[playerInfo.id];
                    }
                }

                // Добавляем или обновляем запись для нового сокета, связывая его с существующим идентификатором игрока
                game.players[socket.id] = playerInfo; // Переиспользуем существующий объект playerInfo
                game.players[socket.id].socket = socket; // Обновляем объект сокета
                // Ensure the identifier and role are correct on the new socket entry
                game.players[socket.id].identifier = identifier; // Make sure identifier is set (уже должно быть, но на всякий случай)
                // playerInfo.id should already be correct (player/opponent role)

                game.playerSockets[playerInfo.id] = socket; // Обновляем ссылку на сокет по роли

                // Убеждаемся, что новый socket.id теперь связан с этой игрой в GameManager - НЕ НУЖНО, socketToGame удален
                // this.socketToGame[socket.id] = game.id;


                // Присоединяем новый сокет к комнате Socket.IO
                socket.join(game.id);
                // --- КОНЕЦ Обновления сокета ---


                // Получаем данные персонажей с точки зрения этого клиента
                // playerInfo.chosenCharacterKey - это персонаж этого клиента
                const playerCharDataForClient = this._getCharacterData(playerInfo.chosenCharacterKey);
                // Определяем ключ персонажа оппонента с точки зрения этого клиента
                const opponentActualSlotId = playerInfo.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
                const opponentCharacterKeyForClient = game.gameState?.[opponentActualSlotId]?.characterKey || null; // Берем из gameState, т.к. там актуальное состояние слотов
                // Если оппонент еще не определен в gameState (PvP ожидание), используем playerCharacterKey/opponentCharacterKey из gameInstance
                // ВАЖНО: при переподключении к *активной* игре, gameState.opponent.characterKey ДОЛЖЕН БЫТЬ определен.
                // Если он null, это может быть PvP ожидание или некорректное состояние.
                if (!opponentCharacterKeyForClient) {
                    // Попробуем найти ключ из GameInstance properties (они устанавливаются при инициализации)
                    const opponentSlotKeyInInstance = playerInfo.id === GAME_CONFIG.PLAYER_ID ? game.playerCharacterKey : game.opponentCharacterKey; // ИСПРАВЛЕНО: Логика получения ключа оппонента
                    opponentCharacterKeyForClient = opponentSlotKeyInInstance;
                    // Если даже из GameInstance properties ключ null, это точно PvP ожидание или критическая ошибка
                }
                const opponentCharDataForClient = this._getCharacterData(opponentCharacterKeyForClient); // Данные оппонента с т.з. клиента


                if (playerCharDataForClient && opponentCharDataForClient && game.gameState) {
                    // Проверяем, готово ли gameState к игре (определены оба бойца)
                    const isGameReadyForPlay = (game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2);
                    const isOpponentDefinedInState = game.gameState.opponent?.characterKey && game.gameState.opponent?.name !== 'Ожидание игрока...';


                    socket.emit('gameState', {
                        gameId: game.id,
                        yourPlayerId: playerInfo.id, // ID слота этого клиента в игре
                        gameState: game.gameState,
                        playerBaseStats: playerCharDataForClient.baseStats, // Статы "моего" персонажа для клиента
                        opponentBaseStats: opponentCharDataForClient.baseStats, // Статы "моего" оппонента для клиента
                        playerAbilities: playerCharDataForClient.abilities, // Абилки "моего" персонажа для клиента
                        opponentAbilities: opponentCharDataForClient.abilities, // Абилки "моего" оппонента для клиента
                        log: game.consumeLogBuffer(), // Отправляем текущий лог и очищаем буфер игры
                        clientConfig: { ...GAME_CONFIG } // Отправляем копию конфига
                    });
                    console.log(`[GameManager] Sent gameState to socket ${socket.id} (identifier: ${identifier}) for game ${game.id}.`);

                    // Логика старта игры при переподключении (если она еще не началась)
                    // Эта логика должна быть только для случая, когда переподключившийся игрок ЗАВЕРШАЕТ состав игры
                    // (например, второй игрок в PvP переподключился к ожидающей игре).
                    // Если игра уже началась, startGame не должен вызываться повторно.
                    // Проверяем: игра не окончена, готова к игре (2 игрока или AI), и состояние оппонента НЕ БЫЛО определено до этого запроса (признак не полностью стартовавшей игры)
                    if (!game.gameState.isGameOver && isGameReadyForPlay && !isOpponentDefinedInState) {
                        console.log(`[GameManager] Game ${game.id} found ready but not fully started on reconnect (Opponent state missing). Initializing/Starting.`);
                        // Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
                        const isInitialized = game.initializeGame(); // Переинициализируем state полностью с обоими персонажами
                        if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
                            game.startGame(); // Запускаем игру (это отправит gameStarted всем, включая этого клиента)
                        } else {
                            console.error(`[GameManager] Failed to initialize game ${game.id} on reconnect. Cannot start.`);
                            // Дополнительная обработка ошибки, возможно, уведомить игроков
                            this.io.to(game.id).emit('gameError', { message: 'Ошибка сервера при старте игры после переподключения. Не удалось инициализировать игру.' });
                            // Если инициализация провалилась, игра в некорректном состоянии, нужно ее удалить
                            this._cleanupGame(gameId, 'reconnect_initialization_failed');
                        }
                    }
                        // Если игра уже активно идет (не окончена, не ожидание) и состояние оппонента БЫЛО определено,
                        // то startGame не вызывается повторно. Клиент получит gameStateUpdate от обычного хода игры.
                    // Если игра PvP ожидающая (1 игрок), startGame не вызывается, isGameReadyForPlay будет false.
                    else if (!isGameReadyForPlay) {
                        console.log(`[GameManager] Reconnected user ${identifier} to pending game ${gameId}. Sending gameState and waiting status.`);
                        // Если это ожидающая игра, убедимся, что клиент получает статус ожидания
                        socket.emit('waitingForOpponent');
                    } else if (game.gameState.isGameOver) {
                        console.log(`[GameManager] Reconnected to game ${gameId} which is already over. Sending gameNotFound.`);
                        // Если игра окончена, client.js должен по gameState.isGameOver показать модалку.
                        // Но чтобы гарантировать возврат в меню при последующих запросах, лучше отправить gameNotFound.
                        // Удаляем ссылку на оконченную игру для этого пользователя
                        delete this.userIdentifierToGameId[identifier];
                        // Отправляем gameNotFound
                        socket.emit('gameNotFound', { message: 'Ваша предыдущая игровая сессия уже завершена.' });
                    } else {
                        // Переподключение к активной игре, которая уже полностью стартовала.
                        console.log(`[GameManager] Reconnected user ${identifier} to active game ${gameId}. gameState sent.`);
                    }


                } else {
                    console.error(`[GameManager] Failed to send gameState to ${socket.id} (identifier ${identifier}) for game ${gameId}: missing character data or gameState.`);
                    socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
                    // Если данные для отправки некорректны, игра в некорректном состоянии, нужно ее удалить
                    this._cleanupGame(gameId, 'reconnect_send_failed');
                    socket.emit('gameNotFound', { message: 'Ваша игровая сессия в некорректном состоянии и была завершена.' });
                }

            } else {
                // Игра найдена по идентификатору пользователя, но игрока с этим идентификатором нет в players этой игры.
                // Это очень странная ситуация, возможно, state userIdentifierToGameId некорректен.
                console.warn(`[GameManager] Found game ${gameId} by identifier ${identifier}, but player with this identifier not found in game.players.`);
                // Удаляем некорректную ссылку и отправляем gameNotFound
                delete this.userIdentifierToGameId[identifier];
                socket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена. Возможно, идентификатор пользователя некорректен.' });
            }
        } else {
            // Игра не найдена по userIdentifierToGameId[identifier]
            console.log(`[GameManager] No active or pending game found for identifier ${identifier}.`);
            socket.emit('gameNotFound', { message: 'Игровая сессия не найдена.' }); // Уведомляем клиента, что игра не найдена
        }
    }

    // --- Вспомогательные функции для получения данных персонажа из data.js ---
    // Скопировано из gameInstance.js, т.к. gameManager тоже использует gameData напрямую
    /**
     * Получает базовые статы и список способностей для персонажа по ключу.
     * Эти функции предназначены для использования ВНУТРИ GameManager или GameInstance.
     * @param {string} key - Ключ персонажа ('elena', 'balard', 'almagest').
     * @returns {{baseStats: object, abilities: array}|null} Объект с базовыми статами и способностями, или null.
     */
    _getCharacterData(key) {
        if (!key) { console.warn("GameManager::_getCharacterData called with null/undefined key."); return null; }
        switch (key) {
            case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities };
            case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities }; // Балард использует opponentAbilities из data.js
            case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities }; // Альмагест использует almagestAbilities из data.js
            default: console.error(`GameManager::_getCharacterData: Unknown character key "${key}"`); return null;
        }
    }
    /**
     * Получает только базовые статы для персонажа по ключу.
     * @param {string} key - Ключ персонажа.
     * @returns {object|null} Базовые статы или null.
     */
    _getCharacterBaseData(key) {
        const charData = this._getCharacterData(key);
        return charData ? charData.baseStats : null;
    }
    /**
     * Получает только список способностей для персонажа по ключу.
     * @param {string} key - Ключ персонажа.
     * @returns {array|null} Список способностей или null.
     */
    _getCharacterAbilities(key) {
        const charData = this._getCharacterData(key);
        return charData ? charData.abilities : null;
    }
}

module.exports = GameManager;