package legion.muyue.best2048;

// --- Imports ---
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.gridlayout.widget.GridLayout;
// Retrofit
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
// OkHttp
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
// Gson
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
// Util
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
// App specific
import legion.muyue.best2048.data.GameInfo;
import legion.muyue.best2048.data.GameStateResponse;
import legion.muyue.best2048.data.MoveRequest;
import legion.muyue.best2048.data.PlayerIdRequest;
import legion.muyue.best2048.network.ApiClient;
import legion.muyue.best2048.network.ApiService;
import legion.muyue.best2048.GameStats;

/**
 * Activité gérant l'interface utilisateur et la logique du mode multijoueur en temps réel du jeu 2048.
 * Cette activité communique avec un serveur backend via une API REST (Retrofit) pour la gestion des parties
 * (création/rejoindre) et une connexion WebSocket (OkHttp) pour la réception des mises à jour de l'état du jeu
 * en temps réel et l'envoi des mouvements du joueur.
 * Elle gère également l'affichage du plateau de jeu, des scores, de l'indicateur de tour,
 * l'état de la connexion, les animations des tuiles, et la détection des gestes de balayage (swipe).
 */
public class MultiplayerActivity extends AppCompatActivity {

    /** Taille du plateau de jeu (4x4). */
    private static final int BOARD_SIZE = 4;
    /** URL du serveur WebSocket pour la communication en temps réel. */
    private static final String WEBSOCKET_URL = "wss://best2048.legion-muyue.fr/ws";
    /** Nombre maximum de tentatives de reconnexion WebSocket. */
    private static final int MAX_RECONNECT_ATTEMPTS = 5;
    /** Délai (en millisecondes) entre les tentatives de reconnexion WebSocket. */
    private static final long RECONNECT_DELAY_MS = 5000;

    // --- UI Elements ---
    /** Layout de grille pour afficher le plateau de jeu. */
    private GridLayout boardGridLayoutMulti;
    /** TextView affichant le score du joueur local. */
    private TextView myScoreLabelMulti;
    /** TextView affichant le score de l'adversaire. */
    private TextView opponentScoreLabelMulti;
    /** TextView indiquant à qui est le tour de jouer. */
    private TextView turnIndicatorMulti;
    /** TextView affichant le statut actuel de la partie multijoueur (recherche, connexion, attente, terminé...). */
    private TextView statusTextMulti;
    /** Indicateur de chargement (ProgressBar). */
    private ProgressBar loadingIndicatorMulti;
    /** Tableau 2D de TextViews représentant les tuiles sur le plateau. */
    private TextView[][] tileViewsMulti = new TextView[BOARD_SIZE][BOARD_SIZE];

    // --- Network Components ---
    /** Service Retrofit pour les appels API REST. */
    private ApiService apiService;
    /** Client OkHttp utilisé pour la connexion WebSocket. */
    private OkHttpClient wsClient;
    /** Instance active de la connexion WebSocket. */
    private WebSocket webSocket;
    /** Listener pour gérer les événements WebSocket. */
    private MyWebSocketListener webSocketListener;
    /** Indicateur de l'état actuel de la connexion WebSocket. */
    private volatile boolean isWebSocketConnected = false;
    /** Compteur des tentatives de reconnexion actuelles. */
    private int reconnectAttempts = 0;
    /** Flag atomique pour éviter les tentatives de reconnexion multiples simultanées. */
    private AtomicBoolean isReconnecting = new AtomicBoolean(false);

    // --- Game State ---
    /** ID de la partie multijoueur en cours. */
    private String currentGameId = null;
    /** ID unique du joueur local. */
    private String myPlayerId = null;
    /** ID de l'adversaire (déduit des informations de la partie). */
    private String opponentPlayerId = null;
    /** Dernier état de jeu reçu du serveur. */
    private GameStateResponse currentGameState = null;
    /** État du plateau précédent, utilisé pour calculer les animations. */
    private int[][] previousBoardState = null;
    /** Indicateur si la partie est terminée (localement ou via le serveur). */
    private volatile boolean gameHasEnded = false;

    // --- Stats & Utilities ---
    /** Instance pour gérer les statistiques de jeu. */
    private GameStats gameStats;
    /** Timestamp du début de la partie multijoueur (pour calculer la durée). */
    private long multiplayerGameStartTimeMs = 0L;
    /** Instance de Gson pour sérialiser/désérialiser les messages WebSocket. */
    private Gson gson;
    /** Handler pour exécuter des tâches sur le thread UI. */
    private Handler uiHandler;
    /** Runnable pour déclencher les tentatives de reconnexion. */
    private Runnable reconnectRunnable;

    /**
     * Méthode appelée lors de la création de l'activité.
     * Initialise l'interface utilisateur, les composants réseau (Retrofit, OkHttpClient, WebSocketListener),
     * les utilitaires (Gson, Handler, GameStats), configure le listener de swipe et lance le processus
     * d'initialisation de la partie multijoueur.
     * Active également le mode EdgeToEdge pour l'affichage.
     *
     * @param savedInstanceState État précédemment sauvegardé de l'activité (non utilisé ici).
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        EdgeToEdge.enable(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multiplayer);

        findViewsMulti(); // Récupère les références des vues
        apiService = ApiClient.getApiService(); // Obtient le service API Retrofit
        // Configure le client OkHttp pour le WebSocket avec un ping interval
        wsClient = new OkHttpClient.Builder().pingInterval(30, TimeUnit.SECONDS).build();
        webSocketListener = new MyWebSocketListener(); // Crée le listener WebSocket
        gson = new Gson(); // Initialise Gson
        gameStats = new GameStats(getApplicationContext()); // Initialise les statistiques
        uiHandler = new Handler(Looper.getMainLooper()); // Initialise le Handler UI
        reconnectRunnable = this::tryReconnect; // Définit le Runnable de reconnexion

        setupSwipeListenerMulti(); // Configure la détection de swipe
        initMultiplayerGame(); // Lance la recherche/création de partie
    }

    /**
     * Méthode appelée lorsque l'activité passe en arrière-plan.
     * Sauvegarde les statistiques, annule les tentatives de reconnexion programmées
     * et ferme la connexion WebSocket pour économiser les ressources.
     */
    @Override
    protected void onPause() {
        super.onPause();
        uiHandler.removeCallbacks(reconnectRunnable); // Annule les reconnexions programmées
        isReconnecting.set(false); // Réinitialise le flag de reconnexion
        closeWebSocket(); // Ferme la connexion WebSocket
        if (gameStats != null) { gameStats.saveStats(); } // Sauvegarde les statistiques
    }

    /**
     * Méthode appelée lorsque l'activité revient au premier plan.
     * Tente de se reconnecter au WebSocket si nécessaire (si une partie était en cours et non terminée)
     * ou rafraîchit l'état du jeu si déjà connecté.
     */
    @Override
    protected void onResume() {
        super.onResume();
        // Si une partie était en cours, non terminée, et qu'on n'est pas connecté ou en reconnexion
        if (currentGameId != null && !isWebSocketConnected && !gameHasEnded && !isReconnecting.get()) {
            resetReconnectionAttempts(); // Réinitialise les tentatives
            tryReconnect(); // Lance la reconnexion
        }
        // Si connecté et partie en cours, rafraîchir l'état au cas où
        else if (currentGameId != null && isWebSocketConnected && !gameHasEnded) {
            fetchGameState();
        }
    }

    /**
     * Méthode appelée avant la destruction de l'activité.
     * Marque la partie comme terminée, annule toutes les tâches du Handler
     * et ferme définitivement la connexion WebSocket.
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        gameHasEnded = true; // Marque la partie comme terminée
        uiHandler.removeCallbacks(reconnectRunnable); // Annule les reconnexions
        closeWebSocket(); // Ferme le WebSocket
        // Supprime tous les messages/callbacks pour éviter les fuites
        if (uiHandler != null) { uiHandler.removeCallbacksAndMessages(null); }
    }

    /**
     * Trouve et assigne les vues de l'interface utilisateur à leurs variables membres correspondantes.
     */
    private void findViewsMulti() {
        boardGridLayoutMulti = findViewById(R.id.gameBoardMulti);
        myScoreLabelMulti = findViewById(R.id.myScoreLabelMulti);
        opponentScoreLabelMulti = findViewById(R.id.opponentScoreLabelMulti);
        turnIndicatorMulti = findViewById(R.id.turnIndicatorMulti);
        statusTextMulti = findViewById(R.id.multiplayerStatusText);
        loadingIndicatorMulti = findViewById(R.id.loadingIndicatorMulti);
    }

    /**
     * Initialise le processus de démarrage d'une partie multijoueur.
     * Affiche l'indicateur de chargement, génère un ID de joueur local, et appelle l'API
     * pour créer une nouvelle partie ou rejoindre une partie existante en attente.
     * En cas de succès, stocke les informations de la partie et lance la connexion WebSocket.
     * Gère les erreurs réseau potentielles.
     */
    private void initMultiplayerGame() {
        gameHasEnded = false; // Assure que le flag est bien à false
        showLoading(true); // Affiche l'indicateur de chargement
        statusTextMulti.setText(R.string.multiplayer_status_searching); // Met à jour le statut
        myPlayerId = java.util.UUID.randomUUID().toString(); // Génère un ID joueur unique

        // Appel API asynchrone pour créer/rejoindre une partie
        apiService.createOrJoinGame(new PlayerIdRequest(myPlayerId)).enqueue(new Callback<GameInfo>() {
            /** Appelée en cas de réponse réussie de l'API. */
            @Override
            public void onResponse(@NonNull Call<GameInfo> call, @NonNull Response<GameInfo> response) {
                if (response.isSuccessful() && response.body() != null) {
                    // Succès : stocke les infos de la partie et lance le WebSocket
                    GameInfo gameInfo = response.body();
                    currentGameId = gameInfo.getGameId();
                    // Détermine l'ID de l'adversaire
                    opponentPlayerId = myPlayerId.equals(gameInfo.getPlayer1Id()) ? gameInfo.getPlayer2Id() : gameInfo.getPlayer1Id();
                    statusTextMulti.setText(getString(R.string.multiplayer_status_found, currentGameId.substring(0, 8)));
                    resetReconnectionAttempts(); // Prépare pour une nouvelle connexion
                    connectWebSocket(); // Établit la connexion WebSocket
                } else {
                    // Erreur de l'API (partie non trouvée, serveur plein, etc.)
                    handleNetworkError(getString(R.string.error_join_create_game, response.code()), true);
                    showLoading(false);
                }
            }
            /** Appelée en cas d'échec de la communication avec l'API. */
            @Override
            public void onFailure(@NonNull Call<GameInfo> call, @NonNull Throwable t) {
                // Erreur réseau ou de connexion au serveur
                handleNetworkError(getString(R.string.error_server_connection), true);
                showLoading(false);
            }
        });
    }

    /**
     * Récupère l'état actuel du jeu depuis le serveur via une requête REST API.
     * Utilisé principalement lors de la reprise de l'activité pour resynchroniser l'état.
     */
    private void fetchGameState() {
        if (currentGameId == null || gameHasEnded) return; // Ne fait rien si pas de partie ou partie terminée
        apiService.getGameState(currentGameId).enqueue(new Callback<GameStateResponse>() {
            @Override
            public void onResponse(@NonNull Call<GameStateResponse> call, @NonNull Response<GameStateResponse> response) {
                if (response.isSuccessful() && response.body() != null && !gameHasEnded) {
                    currentGameState = response.body(); // Met à jour l'état local
                    // Met à jour l'UI sur le thread principal
                    runOnUiThread(() -> updateMultiplayerUI(currentGameState));
                } else {
                    // Gérer l'erreur de récupération ou si la partie s'est terminée entre temps
                }
            }
            @Override
            public void onFailure(@NonNull Call<GameStateResponse> call, @NonNull Throwable t) {
                // Gérer l'échec de la récupération si nécessaire (ex: afficher un message)
            }
        });
    }

    /**
     * Tente d'établir une connexion WebSocket avec le serveur.
     * S'assure qu'aucune connexion n'est déjà active et que la partie n'est pas terminée.
     * Ferme toute connexion précédente avant d'en ouvrir une nouvelle.
     * Met à jour l'UI pour indiquer l'état de connexion.
     */
    private void connectWebSocket() {
        // Conditions pour ne pas établir de connexion
        if (currentGameId == null || myPlayerId == null || isWebSocketConnected || gameHasEnded) {
            return;
        }
        // Ferme l'ancien socket s'il existe
        if (webSocket != null) { closeWebSocket(); }

        // Met à jour l'UI sur le thread principal
        runOnUiThread(() -> {
            statusTextMulti.setText(R.string.multiplayer_status_connecting);
            showLoading(true);
            disableSwipe(); // Désactive les swipes pendant la connexion
        });

        // Crée la requête WebSocket et lance la connexion
        Request request = new Request.Builder().url(WEBSOCKET_URL).build();
        webSocket = wsClient.newWebSocket(request, webSocketListener);
    }

    /**
     * Ferme proprement la connexion WebSocket si elle est active.
     * Réinitialise les indicateurs d'état de connexion et de reconnexion.
     * Annule les tentatives de reconnexion programmées.
     */
    private void closeWebSocket() {
        if (webSocket != null) {
            webSocket.close(1000, "Client closing"); // Code 1000: fermeture normale
            webSocket = null; // Libère la référence
        }
        isWebSocketConnected = false; // Met à jour le flag
        isReconnecting.set(false); // Arrête le processus de reconnexion
        uiHandler.removeCallbacks(reconnectRunnable); // Annule les tâches de reconnexion
    }

    /**
     * Envoie un message (sérialisé en JSON) via la connexion WebSocket active.
     * Ne fait rien si le WebSocket n'est pas connecté ou si la partie est terminée.
     *
     * @param jsonMessage Le message au format JSON à envoyer.
     */
    private void sendWebSocketMessage(String jsonMessage) {
        if (webSocket != null && isWebSocketConnected && !gameHasEnded) {
            webSocket.send(jsonMessage);
        }
    }

    /**
     * Réinitialise le compteur de tentatives de reconnexion et le flag atomique associé.
     * Annule également toute tâche de reconnexion programmée dans le Handler.
     */
    private void resetReconnectionAttempts() {
        reconnectAttempts = 0;
        isReconnecting.set(false);
        uiHandler.removeCallbacks(reconnectRunnable);
    }

    /**
     * Tente de se reconnecter au WebSocket si la connexion est perdue et que le nombre maximal
     * de tentatives n'a pas été atteint.
     * Met à jour l'UI pour indiquer la tentative en cours.
     * Si le nombre maximal est atteint, gère l'erreur comme fatale.
     */
    private void tryReconnect() {
        // Si non connecté, partie non terminée et tentatives restantes
        if (!isWebSocketConnected && !gameHasEnded && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
            isReconnecting.set(true); // Marque comme en cours de reconnexion
            reconnectAttempts++;
            runOnUiThread(()-> statusTextMulti.setText("Disconnected. Reconnecting attempt " + reconnectAttempts + "..."));
            connectWebSocket(); // Lance une nouvelle tentative de connexion
            // La prochaine tentative sera programmée par onFailure si celle-ci échoue
        } else if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
            // Nombre maximal de tentatives atteint
            isReconnecting.set(false);
            handleNetworkError("Reconnection failed. Please check connection and restart.", true);
        } else {
            // Cas où la connexion a été rétablie entre temps ou la partie s'est terminée
            isReconnecting.set(false);
        }
    }

    /**
     * Listener interne pour gérer les événements du cycle de vie et les messages de la connexion WebSocket.
     */
    private class MyWebSocketListener extends WebSocketListener {

        /** Appelée lorsque la connexion WebSocket est établie avec succès. */
        @Override
        public void onOpen(@NonNull WebSocket ws, @NonNull okhttp3.Response response) {
            super.onOpen(ws, response);
            isWebSocketConnected = true; // Met à jour le flag
            resetReconnectionAttempts(); // Réinitialise les tentatives car connecté

            // Envoie un message d'enregistrement au serveur avec l'ID du joueur et de la partie
            RegisterMessage registerMsg = new RegisterMessage("register", myPlayerId, currentGameId);
            ws.send(gson.toJson(registerMsg));

            // Met à jour l'UI sur le thread principal
            runOnUiThread(() -> {
                showLoading(false); // Cache l'indicateur de chargement
                statusTextMulti.setText(R.string.multiplayer_status_waiting_state);
                Toast.makeText(MultiplayerActivity.this, R.string.websocket_connected, Toast.LENGTH_SHORT).show();
                disableSwipe(); // Désactive les swipes en attendant le premier état du jeu
            });
        }

        /** Appelée lorsqu'un message texte est reçu via WebSocket. */
        @Override
        public void onMessage(@NonNull WebSocket ws, @NonNull String text) {
            super.onMessage(ws, text);
            try {
                if (gameHasEnded) return; // Ignore les messages si la partie est déjà marquée comme terminée localement

                // Désérialise le message de base pour déterminer son type
                BaseMessage baseMessage = gson.fromJson(text, BaseMessage.class);

                if ("gameStateUpdate".equals(baseMessage.type)) {
                    // Si c'est une mise à jour de l'état du jeu
                    if (currentGameState != null) {
                        // Sauvegarde l'état précédent pour les animations
                        previousBoardState = currentGameState.getBoard();
                    }
                    // Désérialise l'état complet du jeu
                    currentGameState = gson.fromJson(text, GameStateResponse.class);
                    // Si c'est le premier état reçu et que la partie n'est pas finie, enregistre l'heure de début
                    if (multiplayerGameStartTimeMs == 0L && !currentGameState.isGameOver()) {
                        multiplayerGameStartTimeMs = System.currentTimeMillis();
                    }

                    // Met à jour l'UI sur le thread principal
                    runOnUiThread(() -> {
                        showLoading(false); // S'assure que le chargement est caché
                        updateMultiplayerUI(currentGameState); // Met à jour l'affichage (scores, plateau...)

                        // Anime les changements sur le plateau
                        if (previousBoardState != null) {
                            animateMultiplayerChanges(previousBoardState, currentGameState.getBoard());
                        } else {
                            // Anime l'apparition initiale
                            animateMultiplayerChanges(new int[BOARD_SIZE][BOARD_SIZE], currentGameState.getBoard());
                        }

                        // Active/désactive le swipe en fonction du tour
                        updateSwipeListenerState(currentGameState);

                        // Vérifie si l'état reçu indique la fin de partie
                        if (currentGameState.isGameOver()) {
                            handleMultiplayerGameOver(currentGameState);
                        }
                    });
                } else if ("error".equals(baseMessage.type)) {
                    // Si c'est un message d'erreur du serveur
                    ErrorMessage errorMsg = gson.fromJson(text, ErrorMessage.class);
                    handleServerError(errorMsg.message); // Gère l'erreur
                } else if ("info".equals(baseMessage.type)) {
                    // Si c'est un message d'information du serveur
                    InfoMessage infoMsg = gson.fromJson(text, InfoMessage.class);
                    // Affiche l'info dans un Toast
                    runOnUiThread(() -> Toast.makeText(MultiplayerActivity.this, "Info: " + infoMsg.message, Toast.LENGTH_SHORT).show());
                }

            } catch (JsonSyntaxException e) {
                // Gère une erreur de désérialisation JSON
            }
        }

        /** Appelée lorsque le serveur initie la fermeture de la connexion. */
        @Override
        public void onClosing(@NonNull WebSocket ws, int code, @NonNull String reason) {
            super.onClosing(ws, code, reason);
            isWebSocketConnected = false; // Marque comme déconnecté
        }

        /** Appelée lorsque la connexion WebSocket est complètement fermée. */
        @Override
        public void onClosed(@NonNull WebSocket ws, int code, @NonNull String reason) {
            super.onClosed(ws, code, reason);
            isWebSocketConnected = false; // Marque comme déconnecté
            webSocket = null; // Libère la référence
            // Si la partie n'est pas censée être terminée, tente de se reconnecter
            if (!gameHasEnded) {
                runOnUiThread(() -> statusTextMulti.setText(getString(R.string.websocket_closed) + " Reconnecting..."));
                // Lance le processus de reconnexion si pas déjà en cours
                if (!isReconnecting.getAndSet(true)) {
                    resetReconnectionAttempts();
                    // Programme la première tentative après un court délai
                    uiHandler.postDelayed(reconnectRunnable, 1000);
                }
            } else {
                // Si la partie est terminée, met juste à jour le statut
                runOnUiThread(() -> {
                    statusTextMulti.setText(getString(R.string.websocket_closed));
                    disableSwipe();
                    showLoading(false);
                });
            }
        }

        /** Appelée en cas d'échec de la connexion ou d'erreur de communication. */
        @Override
        public void onFailure(@NonNull WebSocket ws, @NonNull Throwable t, @Nullable okhttp3.Response response) {
            super.onFailure(ws, t, response);
            isWebSocketConnected = false; // Marque comme déconnecté
            webSocket = null; // Libère la référence
            // Si la partie n'est pas terminée, lance ou continue le processus de reconnexion
            if (!gameHasEnded) {
                if (!isReconnecting.get()) { // Si ce n'est pas déjà en cours de reconnexion
                    isReconnecting.set(true); // Marque comme en cours
                    runOnUiThread(() -> {
                        statusTextMulti.setText("Connection failed. Retrying...");
                        disableSwipe();
                        tryReconnect(); // Lance immédiatement la première tentative
                    });
                }
                // Programme la PROCHAINE tentative après le délai configuré
                uiHandler.postDelayed(reconnectRunnable, RECONNECT_DELAY_MS);
            } else {
                // Si la partie est terminée, gère comme une erreur réseau non fatale
                handleNetworkError("WebSocket connection failed.", false);
            }
        }
    }

    // --- Classes internes pour les messages WebSocket ---
    /** Classe de base pour les messages WebSocket, contenant uniquement le type. */
    private static class BaseMessage { String type; }
    /** Message WebSocket pour signaler une erreur du serveur. */
    private static class ErrorMessage extends BaseMessage { String message; }
    /** Message WebSocket pour envoyer une information textuelle. */
    private static class InfoMessage extends BaseMessage { String message; }
    /** Message WebSocket envoyé par le client pour s'enregistrer auprès du serveur. */
    private static class RegisterMessage extends BaseMessage { String playerId; String gameId; RegisterMessage(String t, String p, String g){type=t; playerId=p; gameId=g;} }
    /** Message WebSocket envoyé par le client pour soumettre un mouvement. */
    private static class MoveMessage extends BaseMessage { String direction; String playerId; MoveMessage(String t, String d, String p){type=t; direction=d; playerId=p;} }

    /**
     * Met à jour l'interface utilisateur globale (scores, indicateur de tour, plateau)
     * en fonction de l'état du jeu fourni.
     * S'assure d'être exécutée sur le thread UI.
     *
     * @param state L'état actuel du jeu {@link GameStateResponse}.
     */
    private void updateMultiplayerUI(GameStateResponse state) {
        if (state == null) return; // Ne fait rien si l'état est null
        // Vérifie si on est sur le thread UI, sinon repost sur le thread UI
        if (Looper.myLooper() != Looper.getMainLooper()) { runOnUiThread(() -> updateMultiplayerUI(state)); return; }

        // Met à jour les labels de score
        myScoreLabelMulti.setText(getString(R.string.multiplayer_my_score, state.getMyScore(myPlayerId)));
        opponentScoreLabelMulti.setText(getString(R.string.multiplayer_opponent_score, state.getOpponentScore(myPlayerId)));

        // Met à jour l'indicateur de tour
        boolean myTurn = myPlayerId != null && myPlayerId.equals(state.getCurrentPlayerId());
        turnIndicatorMulti.setText(myTurn ? R.string.multiplayer_turn_yours : R.string.multiplayer_turn_opponent);
        // Change la couleur du texte en fonction du tour
        turnIndicatorMulti.setTextColor(ContextCompat.getColor(this, myTurn ? R.color.tile_16 : R.color.text_tile_low));

        // Synchronise l'affichage du plateau de jeu
        syncBoardViewMulti(state.getBoard());

        // Met à jour le texte de statut si la partie n'est pas terminée
        if (!gameHasEnded && !state.isGameOver()) {
            updateStatusBasedOnTurn(state);
        }
    }

    /**
     * Gère la fin d'une partie multijoueur.
     * Marque la partie comme terminée, enregistre les statistiques finales (victoire, défaite, nul),
     * met à jour l'UI pour afficher le message de fin de partie, désactive les swipes et ferme le WebSocket.
     * S'assure que cela ne se produit qu'une seule fois.
     *
     * @param finalState L'état final du jeu {@link GameStateResponse} reçu du serveur.
     */
    private void handleMultiplayerGameOver(GameStateResponse finalState) {
        // Vérifie si la fin de partie n'a pas déjà été gérée
        if (!gameHasEnded) {
            gameHasEnded = true; // Marque comme terminée
            // Enregistre les statistiques si la partie avait démarré
            if (gameStats != null && multiplayerGameStartTimeMs > 0) {
                long durationMs = System.currentTimeMillis() - multiplayerGameStartTimeMs;
                int myFinalScore = finalState.getMyScore(myPlayerId);
                // Détermine le résultat et enregistre les stats
                if ("DRAW".equals(finalState.getWinnerId())) { gameStats.recordMultiplayerDraw(myFinalScore, durationMs); }
                else if (myPlayerId != null && myPlayerId.equals(finalState.getWinnerId())) { gameStats.recordMultiplayerWin(myFinalScore, durationMs); }
                else { gameStats.recordMultiplayerLoss(myFinalScore, durationMs); }
            }
            // Met à jour l'UI sur le thread principal
            runOnUiThread(() -> {
                showEndGameDialog(finalState);
                disableSwipe(); // Désactive les swipes
                closeWebSocket(); // Ferme la connexion
            });
        }
    }

    /**
     * Synchronise la vue du plateau de jeu ({@link #boardGridLayoutMulti}) avec l'état
     * du plateau fourni.
     * Supprime toutes les vues existantes et recrée les cellules de fond et les TextViews des tuiles
     * en fonction des valeurs dans {@code boardState}.
     *
     * @param boardState Le tableau 2D représentant l'état actuel du plateau.
     */
    private void syncBoardViewMulti(int[][] boardState) {
        if (boardState == null || boardState.length != BOARD_SIZE || boardState[0].length != BOARD_SIZE) { return; }
        boardGridLayoutMulti.removeAllViews(); // Efface le contenu précédent de la grille
        tileViewsMulti = new TextView[BOARD_SIZE][BOARD_SIZE]; // Réinitialise le tableau de références des vues de tuiles
        // Parcours chaque cellule du plateau
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                // Crée et ajoute la cellule de fond (toujours présente)
                View backgroundCell = new View(this);
                backgroundCell.setBackgroundResource(R.drawable.tile_background);
                backgroundCell.getBackground().setTintList(ContextCompat.getColorStateList(this, R.color.tile_empty));
                // Configure les paramètres de layout pour la grille (poids égal pour toutes les cellules)
                GridLayout.LayoutParams bgParams = new GridLayout.LayoutParams(GridLayout.spec(r, 1f), GridLayout.spec(c, 1f));
                bgParams.width = 0; bgParams.height = 0; // Utilise le poids pour déterminer la taille
                int margin = (int) getResources().getDimension(R.dimen.tile_margin); // Marge autour des cellules
                bgParams.setMargins(margin, margin, margin, margin);
                backgroundCell.setLayoutParams(bgParams);
                boardGridLayoutMulti.addView(backgroundCell);

                int value = boardState[r][c]; // Valeur de la tuile à cette position
                if (value > 0) {
                    // Si la cellule contient une tuile, crée le TextView correspondant
                    TextView tileView = createTileTextViewMulti(value, r, c);
                    tileViewsMulti[r][c] = tileView; // Stocke la référence
                    boardGridLayoutMulti.addView(tileView); // Ajoute la vue de la tuile à la grille
                } else {
                    // S'il n'y a pas de tuile, stocke null dans le tableau de références
                    tileViewsMulti[r][c] = null;
                }
            }
        }
    }

    /**
     * Crée et configure une instance de {@link TextView} pour représenter une tuile du jeu.
     * Applique le style approprié (couleur, taille de texte) en fonction de la valeur de la tuile
     * et configure ses paramètres de layout pour le {@link GridLayout}.
     *
     * @param value La valeur de la tuile à afficher.
     * @param row   La ligne de la tuile dans la grille.
     * @param col   La colonne de la tuile dans la grille.
     * @return Le {@link TextView} configuré représentant la tuile.
     */
    private TextView createTileTextViewMulti(int value, int row, int col) {
        TextView tileTextView = new TextView(this); // Crée une nouvelle instance
        setTileStyleMulti(tileTextView, value); // Applique le style (couleur, texte, taille)
        // Configure les paramètres de layout pour la grille (identiques à la cellule de fond)
        GridLayout.LayoutParams params = new GridLayout.LayoutParams(GridLayout.spec(row, 1f), GridLayout.spec(col, 1f));
        params.width = 0; params.height = 0;
        int margin = (int) getResources().getDimension(R.dimen.tile_margin);
        params.setMargins(margin, margin, margin, margin);
        tileTextView.setLayoutParams(params);
        return tileTextView;
    }

    /**
     * Applique le style visuel (couleur de fond, couleur de texte, taille de texte)
     * à un {@link TextView} représentant une tuile, en fonction de sa valeur numérique.
     * Utilise des ressources de couleur et de dimension définies dans les fichiers XML.
     *
     * @param tileTextView Le {@link TextView} de la tuile à styliser.
     * @param value La valeur numérique de la tuile.
     */
    private void setTileStyleMulti(TextView tileTextView, int value) {
        tileTextView.setText(value > 0 ? String.valueOf(value) : ""); // Affiche la valeur ou rien si 0
        tileTextView.setGravity(Gravity.CENTER); // Centre le texte
        tileTextView.setTypeface(null, android.graphics.Typeface.BOLD); // Met le texte en gras

        int backgroundColorId; int textColorId; int textSizeId;
        // Sélectionne les ressources de style en fonction de la valeur
        switch (value) {
            case 0:   backgroundColorId = R.color.tile_empty; textColorId = android.R.color.transparent; textSizeId = R.dimen.text_size_tile_small; break;
            case 2:   backgroundColorId = R.color.tile_2;     textColorId = R.color.text_tile_low;    textSizeId = R.dimen.text_size_tile_small; break;
            case 4:   backgroundColorId = R.color.tile_4;     textColorId = R.color.text_tile_low;    textSizeId = R.dimen.text_size_tile_small; break;
            case 8:   backgroundColorId = R.color.tile_8;     textColorId = R.color.text_tile_high;   textSizeId = R.dimen.text_size_tile_small; break;
            case 16:  backgroundColorId = R.color.tile_16;    textColorId = R.color.text_tile_high;   textSizeId = R.dimen.text_size_tile_small; break;
            case 32:  backgroundColorId = R.color.tile_32;    textColorId = R.color.text_tile_high;   textSizeId = R.dimen.text_size_tile_small; break;
            case 64:  backgroundColorId = R.color.tile_64;    textColorId = R.color.text_tile_high;   textSizeId = R.dimen.text_size_tile_small; break;
            case 128: backgroundColorId = R.color.tile_128;   textColorId = R.color.text_tile_high;   textSizeId = R.dimen.text_size_tile_medium; break;
            case 256: backgroundColorId = R.color.tile_256;   textColorId = R.color.text_tile_high;   textSizeId = R.dimen.text_size_tile_medium; break;
            case 512: backgroundColorId = R.color.tile_512;   textColorId = R.color.text_tile_high;   textSizeId = R.dimen.text_size_tile_medium; break;
            case 1024:backgroundColorId = R.color.tile_1024;  textColorId = R.color.text_tile_high;   textSizeId = R.dimen.text_size_tile_large; break;
            case 2048:backgroundColorId = R.color.tile_2048;  textColorId = R.color.text_tile_high;   textSizeId = R.dimen.text_size_tile_large; break;
            default:  backgroundColorId = R.color.tile_super;   textColorId = R.color.text_tile_high;   textSizeId = R.dimen.text_size_tile_large; break; // Pour > 2048
        }
        // Applique les styles
        tileTextView.setBackgroundResource(R.drawable.tile_background); // Définit le drawable de fond (peut-être arrondi)
        tileTextView.getBackground().setTint(ContextCompat.getColor(this, backgroundColorId)); // Applique la couleur de fond
        tileTextView.setTextColor(ContextCompat.getColor(this, textColorId)); // Applique la couleur du texte
        tileTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(textSizeId)); // Applique la taille du texte
    }

    /**
     * Détecte les changements entre deux états du plateau (avant et après un mouvement)
     * et lance les animations correspondantes (apparition de nouvelles tuiles, fusion de tuiles).
     *
     * @param boardBefore L'état du plateau avant le mouvement.
     * @param boardAfter L'état du plateau après le mouvement.
     */
    private void animateMultiplayerChanges(@NonNull int[][] boardBefore, @NonNull int[][] boardAfter) {
        List<Animator> animations = new ArrayList<>(); // Liste pour collecter les animations à jouer simultanément
        // Parcours toutes les cellules du plateau après le mouvement
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                TextView currentView = tileViewsMulti[r][c]; // Vue de la tuile à cette position (peut être null)
                if (currentView == null) continue; // Ignore si pas de tuile après le mouvement

                int valueAfter = boardAfter[r][c]; // Valeur après
                // Récupère la valeur avant (gère les cas où boardBefore pourrait être mal formé ou null)
                int valueBefore = (boardBefore != null && boardBefore.length > r && boardBefore[r].length > c) ? boardBefore[r][c] : 0;

                // Détecte une nouvelle tuile apparue (était 0 avant, > 0 après)
                if (valueBefore == 0 && valueAfter > 0) {
                    // Prépare la vue pour l'animation d'apparition (petite échelle, transparente)
                    currentView.setScaleX(0.3f); currentView.setScaleY(0.3f); currentView.setAlpha(0f);
                    animations.add(createAppearAnimation(currentView)); // Ajoute l'animation d'apparition
                }
                // Détecte une fusion (valeur après > valeur avant, et n'était pas 0 avant)
                else if (valueAfter > valueBefore && valueBefore != 0) {
                    animations.add(createMergeAnimation(currentView)); // Ajoute l'animation de fusion
                }
            }
        }
        // Si des animations ont été trouvées, les joue toutes ensemble
        if (!animations.isEmpty()) {
            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.playTogether(animations); // Joue toutes les animations simultanément
            animatorSet.start(); // Démarre l'ensemble d'animations
        }
    }

    /**
     * Crée un {@link Animator} pour l'animation d'apparition d'une tuile (scale + alpha).
     *
     * @param view La {@link View} (TextView de la tuile) à animer.
     * @return L'Animator configuré pour l'apparition.
     */
    private Animator createAppearAnimation(@NonNull View view) {
        // Animations de changement d'échelle et d'opacité
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 0.3f, 1f);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 0.3f, 1f);
        ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
        // Groupe les animations pour les jouer ensemble
        AnimatorSet set = new AnimatorSet();
        set.playTogether(scaleX, scaleY, alpha);
        set.setDuration(150); // Durée de l'animation en ms
        return set;
    }

    /**
     * Crée un {@link Animator} pour l'animation de fusion d'une tuile (effet de "pop").
     *
     * @param view La {@link View} (TextView de la tuile) à animer.
     * @return L'Animator configuré pour la fusion.
     */
    private Animator createMergeAnimation(@NonNull View view) {
        // Animation de changement d'échelle (1 -> 1.2 -> 1)
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.2f, 1f);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.2f, 1f);
        // Groupe les animations
        AnimatorSet set = new AnimatorSet();
        set.playTogether(scaleX, scaleY);
        set.setDuration(120); // Durée de l'animation en ms
        return set;
    }

    /**
     * Configure le listener de détection de swipe sur la grille du plateau de jeu.
     * Utilise {@link OnSwipeTouchListener}.
     */
    @SuppressLint("ClickableViewAccessibility") // Nécessaire car on attache un OnTouchListener
    private void setupSwipeListenerMulti() {
        if (boardGridLayoutMulti == null) return; // Vérifie si la grille existe
        // Crée et attache une nouvelle instance de OnSwipeTouchListener
        boardGridLayoutMulti.setOnTouchListener(new OnSwipeTouchListener(this, new OnSwipeTouchListener.SwipeListener() {
            @Override public void onSwipeTop() { handleMultiplayerSwipe(Direction.UP); }
            @Override public void onSwipeBottom() { handleMultiplayerSwipe(Direction.DOWN); }
            @Override public void onSwipeLeft() { handleMultiplayerSwipe(Direction.LEFT); }
            @Override public void onSwipeRight() { handleMultiplayerSwipe(Direction.RIGHT); }
        }));
    }

    /**
     * Gère un geste de balayage (swipe) détecté sur le plateau de jeu.
     * Vérifie si c'est le tour du joueur local et si la partie est en cours.
     * Si les conditions sont remplies, met à jour le statut, désactive temporairement les swipes,
     * et envoie le mouvement au serveur via WebSocket.
     * Affiche un message si ce n'est pas le tour du joueur.
     *
     * @param direction La direction du balayage détecté.
     */
    private void handleMultiplayerSwipe(Direction direction) {
        // Vérifications préliminaires
        if (!isWebSocketConnected || gameHasEnded || currentGameState == null || currentGameState.isGameOver() || myPlayerId == null) return;
        // Vérifie si c'est le tour du joueur local
        if (!myPlayerId.equals(currentGameState.getCurrentPlayerId())) {
            Toast.makeText(this, R.string.error_not_your_turn, Toast.LENGTH_SHORT).show();
            return;
        }

        // C'est le tour du joueur : prépare l'envoi du mouvement
        statusTextMulti.setText(R.string.multiplayer_status_sending_move); // Met à jour le statut
        disableSwipe(); // Désactive les swipes en attendant la réponse

        // Crée le message de mouvement et l'envoie via WebSocket
        MoveMessage moveMsg = new MoveMessage("move", direction.name(), myPlayerId);
        sendWebSocketMessage(gson.toJson(moveMsg));
    }

    /**
     * Gère les messages d'erreur reçus du serveur via WebSocket.
     * Détermine si l'erreur est fatale (nécessite d'arrêter la partie) ou non.
     * Affiche un message approprié à l'utilisateur (Toast ou AlertDialog).
     *
     * @param message Le message d'erreur reçu du serveur.
     */
    private void handleServerError(String message) {
        boolean isFatal = false; // Indicateur si l'erreur bloque la partie
        String userMessage = getString(R.string.server_error_prefix, message); // Message par défaut

        // Détermine si l'erreur est fatale en fonction de son contenu
        if (message.contains("Game not found") || message.contains("Player ID mismatch") || message.contains("Registration failed")) {
            isFatal = true;
            userMessage = "Critical Error: " + message + ". Cannot continue game.";
        } else if (message.contains("Game not currently playing") || message.contains("Game is already over")) {
            // Erreur indiquant que la partie est terminée côté serveur
            isFatal = true;
            userMessage = "Info: " + message;
            gameHasEnded = true; // Marque la partie comme terminée localement aussi
        }

        if (isFatal) {
            // Gère l'erreur fatale (affiche un dialogue bloquant)
            handleNetworkError(userMessage, true);
        } else {
            // Pour les erreurs non fatales, affiche un Toast et remet le statut UI
            String finalUserMessage = userMessage;
            runOnUiThread(() -> {
                Toast.makeText(this, finalUserMessage, Toast.LENGTH_SHORT).show();
                // Si l'erreur est survenue après l'envoi d'un coup, remet le statut "en attente" ou "votre tour"
                if (statusTextMulti.getText().toString().equals(getString(R.string.multiplayer_status_sending_move))) {
                    updateStatusBasedOnTurn(currentGameState); // Remet le statut correct
                }
                // Réactive le swipe si c'était le tour du joueur (au cas où l'erreur était temporaire)
                updateSwipeListenerState(currentGameState);
            });
        }
    }

    /**
     * Active ou désactive la détection des swipes sur le plateau de jeu en fonction
     * de l'état actuel du jeu (si la partie est en cours et si c'est le tour du joueur local).
     *
     * @param state L'état actuel du jeu {@link GameStateResponse}.
     */
    private void updateSwipeListenerState(GameStateResponse state) {
        // Désactive si état null, partie terminée ou finie
        if (state == null || gameHasEnded || state.isGameOver()) {
            disableSwipe();
            return;
        }
        // Active seulement si c'est le tour du joueur local
        boolean myTurn = myPlayerId != null && myPlayerId.equals(state.getCurrentPlayerId());
        if (myTurn) {
            enableSwipe();
        } else {
            disableSwipe();
        }
    }

    /**
     * Active la détection des swipes en attachant le {@link OnSwipeTouchListener} à la grille.
     */
    @SuppressLint("ClickableViewAccessibility") // Nécessaire pour setOnTouchListener
    private void enableSwipe() {
        if (boardGridLayoutMulti != null) {
            // Réattache le listener existant ou en crée un nouveau si nécessaire
            setupSwipeListenerMulti();
        }
    }

    /**
     * Désactive la détection des swipes en détachant le listener (mettant OnTouchListener à null).
     */
    private void disableSwipe() {
        if (boardGridLayoutMulti != null) {
            boardGridLayoutMulti.setOnTouchListener(null); // Détache le listener
        }
    }

    /**
     * Affiche ou masque l'indicateur de chargement (ProgressBar).
     *
     * @param show {@code true} pour afficher l'indicateur, {@code false} pour le masquer.
     */
    private void showLoading(boolean show) {
        if (loadingIndicatorMulti != null) {
            loadingIndicatorMulti.setVisibility(show ? View.VISIBLE : View.GONE);
        }
    }

    /**
     * Gère l'affichage des erreurs réseau ou serveur.
     * Pour les erreurs fatales, affiche un {@link AlertDialog} bloquant qui ferme l'activité.
     * Pour les erreurs non fatales, affiche un {@link Toast} et met à jour le texte de statut.
     * Masque toujours l'indicateur de chargement.
     * S'assure d'être exécutée sur le thread UI.
     *
     * @param message Le message d'erreur à afficher.
     * @param isFatal {@code true} si l'erreur empêche la continuation de la partie.
     */
    private void handleNetworkError(String message, boolean isFatal) {
        runOnUiThread(() -> {
            // Si l'erreur est fatale et l'activité n'est pas en train de se fermer
            if(isFatal && !isFinishing()) {
                // Affiche un dialogue d'erreur bloquant
                new AlertDialog.Builder(this)
                        .setTitle("Error")
                        .setMessage(message)
                        .setPositiveButton(android.R.string.ok, (dialog, which) -> finish()) // Ferme l'activité au clic sur OK
                        .setCancelable(false) // Empêche de fermer le dialogue autrement
                        .show();
                gameHasEnded = true; // Marque la partie comme terminée
                disableSwipe(); // Désactive les interactions
            } else if (!isFinishing()){ // Si erreur non fatale et activité active
                // Met à jour le texte de statut et affiche un Toast long
                if (statusTextMulti != null) { statusTextMulti.setText(message); }
                Toast.makeText(MultiplayerActivity.this, message, Toast.LENGTH_LONG).show();
            }
            showLoading(false); // Masque l'indicateur de chargement dans tous les cas d'erreur gérés
        });
    }

    /**
     * Met à jour le texte de statut principal en fonction de l'état actuel du jeu,
     * typiquement pour indiquer si le joueur attend le coup de l'adversaire.
     * Ne fait rien si la partie est terminée.
     *
     * @param state L'état actuel du jeu {@link GameStateResponse}.
     */
    private void updateStatusBasedOnTurn(GameStateResponse state) {
        // Ne met pas à jour si l'état est null, la partie terminée localement ou via l'état
        if (state == null || state.isGameOver() || gameHasEnded) return;
        // Détermine si c'est le tour du joueur local
        boolean myTurn = myPlayerId != null && myPlayerId.equals(state.getCurrentPlayerId());
        // Affiche "En attente de l'adversaire" si ce n'est pas notre tour, sinon efface le statut (ou le laisse tel quel)
        statusTextMulti.setText(myTurn ? "" : getString(R.string.multiplayer_status_waiting_opponent));
    }

    /**
     * Affiche une boîte de dialogue indiquant la fin de la partie multijoueur (Victoire, Défaite, Égalité)
     * en utilisant un layout personnalisé pour correspondre au style des dialogues solo.
     * Propose un bouton pour revenir à l'écran principal (MainActivity).
     *
     * @param finalState L'état final du jeu reçu du serveur.
     */
    private void showEndGameDialog(GameStateResponse finalState) {
        if (isFinishing() || finalState == null || myPlayerId == null) {
            return;
        }

        // Détermine le résultat
        boolean isWin = myPlayerId.equals(finalState.getWinnerId());
        boolean isDraw = "DRAW".equals(finalState.getWinnerId());

        String titleText;
        String messageText;

        if (isWin) {
            titleText = getString(R.string.game_over_won);
            messageText = getString(R.string.multiplayer_win_message);
        } else if (isDraw) {
            titleText = getString(R.string.game_over_draw);
            messageText = getString(R.string.multiplayer_draw_message);
        } else {
            titleText = getString(R.string.game_over_lost);
            messageText = getString(R.string.multiplayer_loss_message);
        }

        // Création du Builder et inflation du layout personnalisé
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        LayoutInflater inflater = getLayoutInflater();
        View dialogView = inflater.inflate(R.layout.dialog_multiplayer_game_over, null); // Utilise le nouveau layout
        builder.setView(dialogView);
        builder.setCancelable(false);

        // Récupération des vues DANS le layout personnalisé
        TextView titleTextView = dialogView.findViewById(R.id.dialogTitleMultiGameOver);
        TextView messageTextView = dialogView.findViewById(R.id.dialogMessageMultiGameOver);
        Button mainMenuButton = dialogView.findViewById(R.id.dialogButtonMainMenu);

        // Définition du contenu des vues
        titleTextView.setText(titleText);
        messageTextView.setText(messageText);

        // Création de la boîte de dialogue AVANT de définir l'OnClickListener du bouton
        final AlertDialog dialog = builder.create();

        // Configuration du bouton DANS le layout personnalisé
        mainMenuButton.setOnClickListener(v -> {
            // Action lorsque l'utilisateur clique sur le bouton "Menu principal"
            dialog.dismiss();
            closeWebSocket();
            Intent intent = new Intent(MultiplayerActivity.this, MainActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
            startActivity(intent);
            finish();
        });

        dialog.show(); // Affiche la boîte de dialogue
    }

    /**
     * Énumération privée représentant les quatre directions de mouvement possibles.
     */
    private enum Direction { UP, DOWN, LEFT, RIGHT }

}