package legion.muyue.best2048;

import static androidx.core.content.ContextCompat.startActivity;

import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.Settings;
import com.google.android.material.switchmaterial.SwitchMaterial;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewStub;
import android.view.animation.AnimationUtils;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.gridlayout.widget.GridLayout;
import android.widget.Button;
import android.widget.Toast;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.ActivityNotFoundException;
import android.media.AudioAttributes;
import android.media.SoundPool;

import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;


/**
 * Activité principale de l'application, gérant le mode de jeu solo 2048.
 * Responsable de l'affichage du plateau de jeu, des scores, de la gestion des entrées utilisateur (swipes, clics sur boutons),
 * de l'intégration avec la logique du jeu (classe {@link Game}), de la gestion des statistiques (classe {@link GameStats}),
 * des effets sonores ({@link SoundPool}), des paramètres (son, notifications), de la sauvegarde et du chargement de l'état du jeu
 * via {@link SharedPreferences}, et de la planification des tâches de notification en arrière-plan avec {@link WorkManager}.
 */
public class MainActivity extends AppCompatActivity {

    /** Taille du plateau de jeu (4x4). */
    private static final int BOARD_SIZE = 4;
    /** Nom du fichier SharedPreferences pour stocker les préférences et l'état du jeu. */
    private static final String PREFS_NAME = "Best2048_Prefs";
    /** Clé SharedPreferences pour le meilleur score. */
    private static final String HIGH_SCORE_KEY = "high_score";
    /** Clé SharedPreferences pour l'état sérialisé du jeu solo. */
    private static final String GAME_STATE_KEY = "game_state";
    /** Clé SharedPreferences pour le timestamp de la dernière partie jouée. */
    private static final String LAST_PLAYED_TIME_KEY = "last_played_time";
    /** Clé SharedPreferences pour l'état d'activation des sons. */
    private static final String SOUND_ENABLED_KEY = "sound_enabled";
    /** Clé SharedPreferences pour l'état d'activation des notifications. */
    private static final String NOTIFICATIONS_ENABLED_KEY = "notifications_enabled";
    /** Tag unique pour identifier le travail périodique de notification dans WorkManager. */
    private static final String PERIODIC_NOTIFICATION_WORK_TAG = "periodic-notification-work";
    /** Intervalle de répétition (en heures) pour le worker de notification. */
    private static final long WORKER_REPEAT_INTERVAL_HOURS = 12;

    // --- UI Elements ---
    /** Layout de grille pour le plateau de jeu. */
    private GridLayout boardGridLayout;
    /** TextView affichant le score actuel. */
    private TextView currentScoreTextView;
    /** TextView affichant le meilleur score. */
    private TextView highestScoreTextView;
    /** Bouton pour démarrer une nouvelle partie. */
    private Button newGameButton;
    /** Bouton pour accéder au mode multijoueur. */
    private Button multiplayerButton;
    /** Bouton pour afficher/masquer les statistiques. */
    private Button statisticsButton;
    /** Bouton pour ouvrir le menu principal. */
    private Button menuButton;
    /** ViewStub pour charger paresseusement le panneau des statistiques. */
    private ViewStub statisticsViewStub;
    /** Référence vers la vue des statistiques une fois chargée. */
    private View inflatedStatsView;
    /** Tableau 2D de TextViews représentant les tuiles sur le plateau. */
    private TextView[][] tileViews = new TextView[BOARD_SIZE][BOARD_SIZE];

    // --- Game Logic & State ---
    /** Instance de la logique du jeu 2048. */
    private Game game;
    /** Instance pour gérer les statistiques de jeu. */
    private GameStats gameStats;
    /** Indicateur si le panneau des statistiques est actuellement visible. */
    private boolean statisticsVisible = false;
    /** Énumération pour suivre l'état global du flux de jeu (en cours, gagné, perdu). */
    private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER }
    /** État actuel du flux de jeu. */
    private GameFlowState currentGameState = GameFlowState.PLAYING;
    /** Indicateur si les notifications sont activées dans les préférences. */
    private boolean notificationsEnabled = false;
    /** Indicateur si les effets sonores sont activés dans les préférences. */
    private boolean soundEnabled = true;
    /** Instance des SharedPreferences. */
    private SharedPreferences preferences;

    // --- Sound ---
    /** Pool pour gérer et jouer les effets sonores. */
    private SoundPool soundPool;
    /** ID du son pour un mouvement de tuile. */
    private int soundMoveId = -1;
    /** ID du son pour une fusion de tuiles. */
    private int soundMergeId = -1;
    /** ID du son pour la victoire (atteinte de 2048). */
    private int soundWinId = -1;
    /** ID du son pour la fin de partie (game over). */
    private int soundGameOverId = -1;
    /** Indicateur si tous les sons ont été chargés avec succès dans le SoundPool. */
    private boolean soundPoolLoaded = false;

    /**
     * Launcher pour gérer le résultat de la demande de permission de notification
     * (requise à partir d'Android 13).
     */
    private final ActivityResultLauncher<String> requestPermissionLauncher =
            registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
                if (isGranted) {
                    // Permission accordée
                    notificationsEnabled = true;
                    saveNotificationPreference(true);
                    Toast.makeText(this, R.string.notifications_enabled, Toast.LENGTH_SHORT).show();
                    schedulePeriodicNotifications(); // Planifie le worker
                } else {
                    // Permission refusée
                    notificationsEnabled = false;
                    saveNotificationPreference(false);
                    updateNotificationSwitchState(false); // Met à jour le switch dans les paramètres si visible
                    Toast.makeText(this, R.string.notifications_disabled, Toast.LENGTH_SHORT).show();
                    cancelPeriodicNotifications(); // Annule le worker
                }
            });

    /**
     * Méthode appelée lors de la création de l'activité.
     * Initialise les vues, le canal de notification, le SoundPool, le jeu et les statistiques (en chargeant l'état sauvegardé si existant),
     * configure les listeners pour les boutons et les swipes, et planifie le worker de notification si nécessaire.
     *
     * @param savedInstanceState État précédemment sauvegardé (non utilisé directement ici, le chargement se fait via SharedPreferences).
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        EdgeToEdge.enable(this); // Active l'affichage bord à bord
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); // Définit le layout de l'activité

        findViews(); // Récupère les références des vues
        NotificationHelper.createNotificationChannel(this); // Crée le canal de notification (requis pour Android O+)
        initializeSoundPool(); // Initialise les effets sonores
        initializeGameAndStats(); // Charge/Initialise le jeu et les statistiques
        setupListeners(); // Configure les listeners des boutons et des swipes

        // Vérifie et planifie le worker de notification au démarrage si les notifications sont activées et la permission accordée (ou non nécessaire)
        if (notificationsEnabled) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
                schedulePeriodicNotifications();
            } else {
                // Cas où les notifications étaient activées mais la permission a été révoquée entre temps
                notificationsEnabled = false;
                saveNotificationPreference(false);
                cancelPeriodicNotifications();
            }
        }
    }

    /**
     * Méthode appelée lorsque l'activité revient au premier plan.
     * Réinitialise l'heure de début de la partie pour le suivi du temps de jeu de la session actuelle.
     * Gère la visibilité du panneau des statistiques s'il était ouvert avant la pause.
     */
    @Override
    protected void onResume() {
        super.onResume();

        // Réinitialise le timer de la partie en cours si le jeu est en état 'PLAYING'
        if (game != null && gameStats != null && currentGameState == GameFlowState.PLAYING) {
            gameStats.setCurrentGameStartTimeMs(System.currentTimeMillis());
        }

        // Gère la ré-affichage du panneau des statistiques s'il était visible
        if (statisticsVisible) {
            if (inflatedStatsView != null) {
                updateStatisticsTextViews(); // Met à jour les données affichées
                inflatedStatsView.setVisibility(View.VISIBLE); // Le rend visible
                multiplayerButton.setVisibility(View.GONE); // Cache le bouton multijoueur
            } else {
                // Cas où la vue a été détruite (peu probable mais possible), referme le panneau
                statisticsVisible = false;
                toggleStatistics();
            }
        } else {
            // S'assure que le panneau des stats est caché et le bouton multijoueur visible
            if (inflatedStatsView != null) {
                inflatedStatsView.setVisibility(View.GONE);
            }
            multiplayerButton.setVisibility(View.VISIBLE);
        }
    }

    /**
     * Méthode appelée lorsque l'activité passe en arrière-plan.
     * Enregistre le temps de jeu de la session en cours, sauvegarde l'état du jeu,
     * les statistiques et le timestamp de la dernière partie jouée dans les SharedPreferences.
     */
    @Override
    protected void onPause() {
        super.onPause();

        if (game != null && gameStats != null) {
            // Si le jeu était en cours, enregistre la durée de la session
            if (currentGameState == GameFlowState.PLAYING) {
                long sessionDuration = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
                gameStats.addPlayTime(sessionDuration);
            }
            saveGame(); // Sauvegarde l'état du plateau et le score actuel
            gameStats.saveStats(); // Sauvegarde toutes les statistiques persistantes
            saveLastPlayedTime(); // Enregistre l'heure actuelle comme dernière heure jouée
        }
    }

    /**
     * Méthode appelée juste avant la destruction de l'activité.
     * Libère les ressources du SoundPool.
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // Libère le SoundPool pour éviter les fuites de mémoire
        if (soundPool != null) {
            soundPool.release();
            soundPool = null;
        }
    }

    /**
     * Récupère les références des vues principales de l'activité depuis le layout XML.
     */
    private void findViews() {
        boardGridLayout = findViewById(R.id.gameBoard);
        currentScoreTextView = findViewById(R.id.scoreLabel);
        highestScoreTextView = findViewById(R.id.highScoreLabel);
        newGameButton = findViewById(R.id.restartButton);
        statisticsButton = findViewById(R.id.statsButton);
        menuButton = findViewById(R.id.menuButton);
        multiplayerButton = findViewById(R.id.multiplayerButton);
        statisticsViewStub = findViewById(R.id.statsViewStub);
    }

    /**
     * Initialise les SharedPreferences, charge les préférences de son et de notification,
     * initialise l'instance de GameStats, et charge l'état du jeu sauvegardé ou démarre une nouvelle partie.
     */
    private void initializeGameAndStats() {
        // Initialise les SharedPreferences
        preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);

        // Charge les préférences utilisateur
        loadSoundPreference();
        loadNotificationPreference();

        // Initialise le gestionnaire de statistiques
        gameStats = new GameStats(this); // Charge automatiquement les stats persistées

        // Charge l'état du jeu sauvegardé
        loadGame();

        // Si aucun jeu n'a été chargé (ou échec), démarre une nouvelle partie
        if (game == null) {
            startNewGame();
        } else {
            // Si un jeu a été chargé, met à jour son meilleur score avec celui des stats globales
            game.setHighestScore(gameStats.getOverallHighScore());
            updateUI(); // Met à jour l'affichage initial
        }
    }

    /**
     * Initialise le SoundPool et charge les différents effets sonores utilisés dans le jeu.
     * Gère les erreurs de chargement et met à jour l'état d'activation du son si nécessaire.
     */
    private void initializeSoundPool() {
        // Configure les attributs audio pour les jeux
        AudioAttributes attributes = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_GAME)
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .build();

        // Crée le SoundPool
        soundPool = new SoundPool.Builder()
                .setMaxStreams(3) // Nombre maximum de sons joués simultanément
                .setAudioAttributes(attributes)
                .build();

        // Définit un listener pour savoir quand les sons sont chargés
        soundPool.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
            if (status == 0) { // 0 signifie succès
                // Vérifie si tous les sons attendus sont chargés
                if (soundMoveId > 0 && soundMergeId > 0 && soundWinId > 0 && soundGameOverId > 0) {
                    soundPoolLoaded = true; // Tous les sons sont prêts
                }
            } else { // Échec du chargement d'un son
                soundEnabled = false; // Désactive les sons
                saveSoundPreference(false); // Sauvegarde la préférence
            }
        });

        // Tente de charger chaque son depuis les ressources raw
        try {
            soundMoveId = soundPool.load(this, R.raw.move, 1);
            soundMergeId = soundPool.load(this, R.raw.merge, 1);
            soundWinId = soundPool.load(this, R.raw.win, 1);
            soundGameOverId = soundPool.load(this, R.raw.game_over, 1);
            // Vérifie si load a retourné 0 (erreur)
            if (soundMoveId == 0 || soundMergeId == 0 || soundWinId == 0 || soundGameOverId == 0) {
                throw new RuntimeException("SoundPool.load returned 0");
            }
        } catch (Exception e) {
            // En cas d'erreur (fichier manquant, etc.), désactive les sons
            soundEnabled = false;
            saveSoundPreference(false);
            Toast.makeText(this, "Error loading sounds, sound disabled.", Toast.LENGTH_LONG).show();
        }
    }

    /**
     * Configure les OnClickListeners pour les boutons de l'interface utilisateur
     * (Nouvelle Partie, Statistiques, Menu, Multijoueur) et configure le listener de swipe.
     */
    private void setupListeners() {
        // Bouton Nouvelle Partie -> Affiche dialogue de confirmation
        newGameButton.setOnClickListener(v -> {
            v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
            showRestartConfirmationDialog();
        });

        // Bouton Statistiques -> Affiche/Masque le panneau des stats
        statisticsButton.setOnClickListener(v -> {
            v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
            toggleStatistics();
        });

        // Bouton Menu -> Affiche le dialogue du menu principal
        menuButton.setOnClickListener(v -> {
            v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
            showMenu();
        });

        // Bouton Multijoueur -> Lance l'activité MultiplayerActivity
        multiplayerButton.setOnClickListener(v -> {
            v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
            showMultiplayerScreen();
        });

        // Configure la détection des swipes sur le plateau
        setupSwipeListener();
    }

    /**
     * Met à jour l'ensemble de l'interface utilisateur principale (plateau, scores).
     * Actualise également le panneau des statistiques s'il est visible.
     */
    private void updateUI() {
        if (game == null) {
            return; // Ne fait rien si le jeu n'est pas initialisé
        }
        syncBoardView(); // Redessine le plateau de jeu
        updateScores(); // Met à jour les scores affichés
        // Met à jour les stats si le panneau est visible
        if (statisticsVisible && inflatedStatsView != null) {
            updateStatisticsTextViews();
        }
    }

    /**
     * Synchronise l'affichage du {@link GridLayout} (plateau) avec l'état actuel du jeu.
     * Supprime toutes les vues précédentes et recrée les cellules de fond et les TextViews des tuiles
     * en fonction des valeurs du plateau de l'objet {@link Game}.
     */
    private void syncBoardView() {
        if (game == null || boardGridLayout == null) {
            return; // Vérifications préliminaires
        }
        boardGridLayout.removeAllViews(); // Efface le contenu actuel de la grille
        // Réinitialise le tableau des références aux vues des tuiles
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                tileViews[r][c] = null;
            }
        }

        int gridMargin = (int) getResources().getDimension(R.dimen.tile_margin); // Marge entre les tuiles
        Context context = this;

        // Crée et ajoute les cellules de fond pour chaque position
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                View backgroundCell = new View(context); // Crée une vue simple
                backgroundCell.setBackgroundResource(R.drawable.tile_background); // Applique le fond arrondi
                backgroundCell.getBackground().setTintList(ContextCompat.getColorStateList(context, R.color.tile_empty)); // Couleur de fond par défaut

                // Configure les paramètres de layout pour la grille (poids égal pour toutes les cellules)
                GridLayout.LayoutParams params = new GridLayout.LayoutParams();
                params.width = 0; params.height = 0; // Taille déterminée par le poids
                params.rowSpec = GridLayout.spec(r, 1, 1f); // Poids 1 en ligne
                params.columnSpec = GridLayout.spec(c, 1, 1f); // Poids 1 en colonne
                params.setMargins(gridMargin, gridMargin, gridMargin, gridMargin); // Applique les marges
                backgroundCell.setLayoutParams(params);
                boardGridLayout.addView(backgroundCell); // Ajoute la cellule de fond à la grille
            }
        }

        // Crée et ajoute les TextViews pour les tuiles ayant une valeur > 0
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                int value = game.getCellValue(r, c); // Récupère la valeur de la tuile
                if (value > 0) {
                    // Crée le TextView pour la tuile
                    TextView tileTextView = createTileTextView(value, r, c);
                    tileViews[r][c] = tileTextView; // Stocke la référence
                    boardGridLayout.addView(tileTextView); // Ajoute la tuile à la grille (par-dessus le fond)
                }
            }
        }
        updateScores(); // Met à jour les scores après avoir redessiné le plateau
    }

    /**
     * Crée une instance de {@link TextView} pour représenter une tuile du jeu.
     * Applique le style approprié et configure les paramètres de layout pour le {@link GridLayout}.
     *
     * @param value La valeur de la tuile.
     * @param row   La ligne de la tuile.
     * @param col   La colonne de la tuile.
     * @return Le {@link TextView} configuré pour la tuile.
     */
    @NonNull
    private TextView createTileTextView(int value, int row, int col) {
        TextView tileTextView = new TextView(this);
        setTileStyle(tileTextView, value); // Applique le style visuel

        // Configure les paramètres de layout (identiques à ceux de la cellule de fond)
        GridLayout.LayoutParams params = new GridLayout.LayoutParams();
        params.width = 0;
        params.height = 0;
        params.rowSpec = GridLayout.spec(row, 1, 1f);
        params.columnSpec = GridLayout.spec(col, 1, 1f);
        int margin = (int) getResources().getDimension(R.dimen.tile_margin);
        params.setMargins(margin, margin, margin, margin);
        tileTextView.setLayoutParams(params);

        return tileTextView;
    }

    /**
     * Met à jour les TextViews affichant le score actuel et le meilleur score.
     */
    private void updateScores() {
        if (game != null && currentScoreTextView != null && highestScoreTextView != null) {
            currentScoreTextView.setText(getString(R.string.score_placeholder, game.getCurrentScore()));
            highestScoreTextView.setText(getString(R.string.high_score_placeholder, game.getHighestScore()));
        }
    }

    /**
     * Applique le style visuel (couleur de fond, couleur et taille du texte) à un TextView de tuile
     * en fonction de sa valeur numérique.
     *
     * @param tileTextView Le TextView à styliser.
     * @param value La valeur de la tuile.
     */
    private void setTileStyle(@NonNull TextView tileTextView, int value) {
        tileTextView.setText(value > 0 ? String.valueOf(value) : ""); // Affiche la valeur ou vide
        tileTextView.setGravity(Gravity.CENTER); // Centre le texte
        tileTextView.setTypeface(null, android.graphics.Typeface.BOLD); // Met en gras

        // Variables pour stocker les ID des ressources de style
        int backgroundColorId;
        int textColorId;
        int textSizeId;

        // Sélectionne les styles en fonction de la valeur de la tuile
        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 les tuiles > 2048
        }

        // Applique les styles sélectionnés
        tileTextView.setBackgroundResource(R.drawable.tile_background); // Applique le drawable de fond
        tileTextView.getBackground().setTint(ContextCompat.getColor(this, backgroundColorId)); // Définit la couleur de fond
        tileTextView.setTextColor(ContextCompat.getColor(this, textColorId)); // Définit la couleur du texte
        tileTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(textSizeId)); // Définit la taille du texte
    }

    /**
     * Configure le listener pour détecter les gestes de balayage (swipe) sur le plateau de jeu.
     * Utilise la classe {@link OnSwipeTouchListener}.
     */
    @SuppressLint("ClickableViewAccessibility") // Requis pour setOnTouchListener
    private void setupSwipeListener() {
        if (boardGridLayout == null) {
            return; // Vérifie si la grille existe
        }
        // Attache le listener à la grille
        boardGridLayout.setOnTouchListener(new OnSwipeTouchListener(this, new OnSwipeTouchListener.SwipeListener() {
            // Appelle handleSwipe avec la direction correspondante pour chaque type de swipe détecté
            @Override public void onSwipeTop()    { handleSwipe(Direction.UP);    }
            @Override public void onSwipeBottom() { handleSwipe(Direction.DOWN);  }
            @Override public void onSwipeLeft()   { handleSwipe(Direction.LEFT);  }
            @Override public void onSwipeRight()  { handleSwipe(Direction.RIGHT); }
        }));
    }

    /**
     * Gère un geste de balayage (swipe) détecté.
     * Appelle la méthode correspondante dans l'objet {@link Game} pour effectuer le mouvement.
     * Si le plateau change, met à jour les statistiques, joue les sons appropriés, ajoute une nouvelle tuile,
     * met à jour l'UI et lance les animations.
     * Vérifie ensuite les conditions de fin de partie (victoire ou game over).
     *
     * @param direction La direction du balayage.
     */
    private void handleSwipe(Direction direction) {
        // Ne fait rien si le jeu n'est pas initialisé, pas de stats, ou si la partie est déjà terminée
        if (game == null || gameStats == null || currentGameState == GameFlowState.GAME_OVER) {
            return;
        }

        int scoreBefore = game.getCurrentScore(); // Score avant le mouvement
        int[][] boardBeforePush = game.getBoard(); // État du plateau avant le mouvement (pour animations)
        boolean boardChanged = false; // Indicateur si le mouvement a modifié le plateau

        // Appelle la méthode de mouvement correspondante dans l'objet Game
        switch (direction) {
            case UP:    boardChanged = game.pushUp();    break;
            case DOWN:  boardChanged = game.pushDown();  break;
            case LEFT:  boardChanged = game.pushLeft();  break;
            case RIGHT: boardChanged = game.pushRight(); break;
        }

        // Si le mouvement a modifié le plateau
        if (boardChanged) {
            playSound(soundMoveId); // Joue le son de mouvement
            int[][] boardAfterPush = game.getBoard(); // État du plateau après le push/merge

            // Met à jour les statistiques
            gameStats.recordMove(); // Enregistre le mouvement
            int scoreAfter = game.getCurrentScore();
            if (scoreAfter > scoreBefore) { // Si le score a augmenté (fusion)
                playSound(soundMergeId); // Joue le son de fusion
                gameStats.recordMerge(1); // Enregistre la fusion (suppose 1 par coup pour simplifier)
                // Met à jour le meilleur score si nécessaire
                if (scoreAfter > game.getHighestScore()) {
                    game.setHighestScore(scoreAfter);
                    gameStats.setHighestScore(scoreAfter);
                }
            }
            gameStats.updateHighestTile(game.getHighestTileValue()); // Met à jour la plus haute tuile atteinte

            game.addNewTile(); // Ajoute une nouvelle tuile aléatoire
            int[][] boardAfterAdd = game.getBoard(); // État final du plateau après ajout

            syncBoardView(); // Met à jour l'affichage du plateau (sans animation ici, car animateChanges le fera)
            animateChanges(boardBeforePush, boardAfterPush, boardAfterAdd); // Lance les animations
            // Les conditions de fin de partie sont vérifiées à la fin des animations

        } else {
            // Si le plateau n'a pas changé, vérifie immédiatement si c'est un game over
            if (game.isGameOver() && currentGameState != GameFlowState.GAME_OVER) {
                currentGameState = GameFlowState.GAME_OVER;
                gameStats.recordLoss(); // Enregistre la défaite
                showGameOverDialog(); // Affiche le dialogue de fin de partie
            }
        }
    }

    /**
     * Joue un effet sonore si les sons sont activés et chargés.
     *
     * @param soundId L'ID du son à jouer (obtenu via {@link SoundPool#load}).
     */
    private void playSound(int soundId) {
        // Vérifie si le pool est chargé, les sons activés, le pool existe et l'ID est valide
        if (soundPoolLoaded && soundEnabled && soundPool != null && soundId > 0) {
            // Joue le son
            soundPool.play(soundId, 1.0f, 1.0f, 1, 0, 1.0f); // volume G/D, priorité, loop, rate
        }
    }

    /**
     * Anime les changements survenus sur le plateau après un mouvement.
     * Détecte les nouvelles tuiles apparues et les tuiles fusionnées et lance les animations correspondantes.
     * Vérifie les conditions de fin de partie une fois les animations terminées.
     *
     * @param boardBeforePush État du plateau avant le mouvement (push/merge).
     * @param boardAfterPush  État du plateau après le mouvement mais avant l'ajout de la nouvelle tuile.
     * @param boardAfterAdd   État final du plateau après l'ajout de la nouvelle tuile.
     */
    private void animateChanges(@NonNull int[][] boardBeforePush, @NonNull int[][] boardAfterPush, @NonNull int[][] boardAfterAdd) {
        List<Animator> animations = new ArrayList<>(); // Liste pour regrouper les animations

        // Parcours le plateau final
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                TextView currentView = tileViews[r][c]; // Vue à cette position
                if (currentView == null) continue; // Passe si pas de vue (cellule vide après coup)

                int valueAfterAdd = boardAfterAdd[r][c]; // Valeur finale
                int valueAfterPush = boardAfterPush[r][c]; // Valeur après push/merge

                // Détecte l'apparition d'une nouvelle tuile (était vide après push, mais a une valeur après add)
                if (valueAfterPush == 0 && valueAfterAdd > 0) {
                    // Prépare l'animation d'apparition
                    currentView.setScaleX(0.3f);
                    currentView.setScaleY(0.3f);
                    currentView.setAlpha(0f);
                    animations.add(createAppearAnimation(currentView));
                }

                // Détecte une fusion (valeur après push > valeur avant push, et n'était pas 0 avant)
                boolean fusionOccurredAtThisPosition = false;
                if (valueAfterPush > boardBeforePush[r][c] && boardBeforePush[r][c] != 0) {
                    fusionOccurredAtThisPosition = true;
                }

                // Ajoute l'animation de fusion si fusion a eu lieu ET si ce n'est pas la tuile qui vient juste d'apparaître
                if (fusionOccurredAtThisPosition && !(valueAfterPush == 0 && valueAfterAdd > 0)) {
                    animations.add(createMergeAnimation(currentView));
                }
            }
        }

        // Si des animations sont à jouer
        if (!animations.isEmpty()) {
            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.playTogether(animations); // Joue toutes les animations ensemble
            // Ajoute un listener pour vérifier la fin de partie APRÈS les animations
            animatorSet.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    checkEndGameConditions(); // Vérifie victoire/défaite
                }
            });
            animatorSet.start(); // Démarre les animations
        } else {
            // S'il n'y a pas d'animations (ex: mouvement sans changement), vérifie quand même la fin de partie
            checkEndGameConditions();
        }
    }

    /**
     * Crée un {@link Animator} pour l'animation d'apparition d'une tuile (scale + alpha).
     *
     * @param view La vue de la tuile à animer.
     * @return L'Animator configuré.
     */
    private Animator createAppearAnimation(@NonNull View view) {
        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);
        AnimatorSet set = new AnimatorSet();
        set.playTogether(scaleX, scaleY, alpha);
        set.setDuration(150); // Durée en ms
        return set;
    }

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

    /**
     * Vérifie si les conditions de fin de partie (victoire ou game over) sont remplies
     * et met à jour l'état du jeu ({@link #currentGameState}) et les statistiques en conséquence.
     * Affiche les dialogues appropriés.
     */
    private void checkEndGameConditions() {
        if (game == null || gameStats == null) {
            return; // Vérifications
        }

        // Vérifie la victoire (si pas déjà dans un état final)
        if (game.isGameWon() && currentGameState == GameFlowState.PLAYING) {
            currentGameState = GameFlowState.WON_DIALOG_SHOWN; // Met à jour l'état
            long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs(); // Calcule le temps
            gameStats.recordWin(timeTaken); // Enregistre la victoire dans les stats
            showGameWonKeepPlayingDialog(); // Affiche le dialogue de victoire
            showAchievementNotification(game.getHighestTileValue()); // Affiche une notification de succès
        }
        // Vérifie le game over (si pas déjà dans un état final)
        else if (game.isGameOver() && currentGameState != GameFlowState.GAME_OVER) {
            currentGameState = GameFlowState.GAME_OVER; // Met à jour l'état
            gameStats.recordLoss(); // Enregistre la défaite dans les stats
            showGameOverDialog(); // Affiche le dialogue de game over
        }
    }

    /**
     * Énumération privée pour représenter les directions de swipe.
     */
    private enum Direction { UP, DOWN, LEFT, RIGHT }

    /**
     * Affiche un dialogue demandant confirmation avant de redémarrer une nouvelle partie.
     */
    private void showRestartConfirmationDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        LayoutInflater inflater = getLayoutInflater();
        // Charge le layout personnalisé du dialogue
        View dialogView = inflater.inflate(R.layout.dialog_restart_confirm, null);
        builder.setView(dialogView);
        builder.setCancelable(false); // Empêche de fermer en cliquant à l'extérieur

        // Récupère les boutons du dialogue
        Button cancelButton = dialogView.findViewById(R.id.dialogCancelButton);
        Button confirmButton = dialogView.findViewById(R.id.dialogConfirmButton);

        final AlertDialog dialog = builder.create(); // Crée le dialogue

        // Actions des boutons
        cancelButton.setOnClickListener(v -> dialog.dismiss()); // Ferme le dialogue
        confirmButton.setOnClickListener(v -> {
            dialog.dismiss(); // Ferme le dialogue
            startNewGame(); // Lance une nouvelle partie
        });
        dialog.show(); // Affiche le dialogue
    }

    /**
     * Démarre une nouvelle partie.
     * Réinitialise l'objet {@link Game}, met à jour les statistiques de début de partie,
     * réinitialise l'état du flux de jeu, ferme le panneau des statistiques s'il est ouvert,
     * et met à jour l'interface utilisateur.
     */
    private void startNewGame() {
        if (gameStats == null) { // S'assure que gameStats est initialisé
            gameStats = new GameStats(this);
        }
        gameStats.startGame(); // Notifie GameStats du début d'une nouvelle partie

        game = new Game(); // Crée une nouvelle instance de jeu
        // Met à jour le meilleur score du jeu avec celui des stats globales
        game.setHighestScore(gameStats.getOverallHighScore());
        currentGameState = GameFlowState.PLAYING; // Réinitialise l'état du jeu

        // Ferme les statistiques si elles sont ouvertes
        if (statisticsVisible) {
            toggleStatistics();
        }
        updateUI(); // Met à jour l'affichage pour la nouvelle partie
    }

    /**
     * Affiche le dialogue indiquant que le joueur a gagné (atteint 2048 ou plus)
     * et lui propose de continuer à jouer ou de commencer une nouvelle partie.
     * Joue également le son de victoire.
     */
    private void showGameWonKeepPlayingDialog() {
        playSound(soundWinId); // Joue le son de victoire

        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        LayoutInflater inflater = getLayoutInflater();
        // Charge le layout personnalisé
        View dialogView = inflater.inflate(R.layout.dialog_game_won, null);
        builder.setView(dialogView);
        builder.setCancelable(false); // Ne peut pas être fermé en cliquant à l'extérieur

        // Récupère les boutons
        Button keepPlayingButton = dialogView.findViewById(R.id.dialogKeepPlayingButton);
        Button newGameButtonDialog = dialogView.findViewById(R.id.dialogNewGameButtonWon);

        final AlertDialog dialog = builder.create();

        // Actions des boutons
        keepPlayingButton.setOnClickListener(v -> dialog.dismiss()); // Ferme juste le dialogue, le jeu continue
        newGameButtonDialog.setOnClickListener(v -> {
            dialog.dismiss(); // Ferme le dialogue
            startNewGame(); // Commence une nouvelle partie
        });
        dialog.show(); // Affiche le dialogue
    }

    /**
     * Affiche le dialogue indiquant que la partie est terminée (game over).
     * Affiche le score final et propose de commencer une nouvelle partie ou de quitter.
     * Joue également le son de fin de partie.
     */
    private void showGameOverDialog() {
        playSound(soundGameOverId); // Joue le son de game over

        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        LayoutInflater inflater = getLayoutInflater();
        // Charge le layout personnalisé
        View dialogView = inflater.inflate(R.layout.dialog_game_over, null);
        builder.setView(dialogView);
        builder.setCancelable(false); // Non annulable

        // Récupère les vues du dialogue
        TextView messageTextView = dialogView.findViewById(R.id.dialogMessageGameOver);
        Button newGameButtonDialog = dialogView.findViewById(R.id.dialogNewGameButtonGameOver);
        Button quitButton = dialogView.findViewById(R.id.dialogQuitButtonGameOver);

        // Affiche le score final dans le message
        if (game != null) {
            messageTextView.setText(getString(R.string.game_over_message, game.getCurrentScore()));
        } else {
            messageTextView.setText(getString(R.string.game_over_title)); // Message générique si jeu null
        }

        final AlertDialog dialog = builder.create();

        // Actions des boutons
        newGameButtonDialog.setOnClickListener(v -> {
            dialog.dismiss();
            startNewGame(); // Nouvelle partie
        });
        quitButton.setOnClickListener(v -> {
            dialog.dismiss();
            finish(); // Ferme l'activité (quitte l'application si c'est la seule activité)
        });
        dialog.show(); // Affiche le dialogue
    }

    /**
     * Affiche le dialogue du menu principal avec les options : Comment Jouer, Paramètres, À Propos, Retour.
     */
    private void showMenu() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        LayoutInflater inflater = getLayoutInflater();
        View dialogView = inflater.inflate(R.layout.dialog_main_menu, null);
        builder.setView(dialogView);
        builder.setCancelable(true); // Peut être fermé en cliquant à l'extérieur

        // Récupère les boutons du menu
        Button howToPlayButton = dialogView.findViewById(R.id.menuButtonHowToPlay);
        Button settingsButton = dialogView.findViewById(R.id.menuButtonSettings);
        Button aboutButton = dialogView.findViewById(R.id.menuButtonAbout);
        Button returnButton = dialogView.findViewById(R.id.menuButtonReturn);

        final AlertDialog dialog = builder.create();

        // Actions des boutons du menu
        howToPlayButton.setOnClickListener(v -> { dialog.dismiss(); showHowToPlayDialog(); });
        settingsButton.setOnClickListener(v -> { dialog.dismiss(); showSettingsDialog(); });
        aboutButton.setOnClickListener(v -> { dialog.dismiss(); showAboutDialog(); });
        returnButton.setOnClickListener(v -> dialog.dismiss()); // Ferme simplement le menu
        dialog.show();
    }

    /**
     * Affiche un dialogue simple expliquant les règles du jeu.
     */
    private void showHowToPlayDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        LayoutInflater inflater = getLayoutInflater();
        View dialogView = inflater.inflate(R.layout.dialog_how_to_play, null);
        builder.setView(dialogView);
        builder.setCancelable(true); // Annulable
        Button okButton = dialogView.findViewById(R.id.dialogOkButtonHowToPlay); // Bouton OK pour fermer
        final AlertDialog dialog = builder.create();
        okButton.setOnClickListener(v -> dialog.dismiss());
        dialog.show();
    }

    /**
     * Affiche le dialogue des paramètres, permettant à l'utilisateur d'activer/désactiver
     * le son et les notifications, de gérer les permissions, de partager ou réinitialiser
     * les statistiques, et de quitter l'application.
     */
    private void showSettingsDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        LayoutInflater inflater = getLayoutInflater();
        View dialogView = inflater.inflate(R.layout.dialog_settings, null);
        builder.setView(dialogView).setCancelable(true); // Annulable

        // Récupère les vues des paramètres
        SwitchMaterial switchSound = dialogView.findViewById(R.id.switchSound);
        SwitchMaterial switchNotifications = dialogView.findViewById(R.id.switchNotifications);
        Button permissionsButton = dialogView.findViewById(R.id.buttonManagePermissions);
        Button shareStatsButton = dialogView.findViewById(R.id.buttonShareStats);
        Button resetStatsButton = dialogView.findViewById(R.id.buttonResetStats);
        Button quitAppButton = dialogView.findViewById(R.id.buttonQuitApp);
        Button closeButton = dialogView.findViewById(R.id.buttonCloseSettings);

        final AlertDialog dialog = builder.create();

        // Configure le switch Son
        switchSound.setChecked(soundEnabled); // État initial
        switchSound.setOnCheckedChangeListener((buttonView, isChecked) -> {
            soundEnabled = isChecked; // Met à jour l'état local
            saveSoundPreference(isChecked); // Sauvegarde la préférence
            // Affiche un Toast de confirmation
            Toast.makeText(this,
                    isChecked ? R.string.sound_enabled : R.string.sound_disabled,
                    Toast.LENGTH_SHORT).show();
        });

        // Configure le switch Notifications
        switchNotifications.setChecked(notificationsEnabled); // État initial
        switchNotifications.setOnCheckedChangeListener((buttonView, isChecked) -> {
            if (isChecked) {
                // Si activé, demande la permission (gère la logique interne)
                requestNotificationPermission();
            } else {
                // Si désactivé
                notificationsEnabled = false;
                saveNotificationPreference(false); // Sauvegarde la préférence
                Toast.makeText(this, R.string.notifications_disabled, Toast.LENGTH_SHORT).show();
                cancelPeriodicNotifications(); // Annule le worker de notification
            }
        });

        // Configure les boutons
        permissionsButton.setOnClickListener(v -> { openAppSettings(); dialog.dismiss(); }); // Ouvre les paramètres système de l'app
        shareStatsButton.setOnClickListener(v -> { shareStats(); dialog.dismiss(); }); // Partage les statistiques
        resetStatsButton.setOnClickListener(v -> { dialog.dismiss(); showResetStatsConfirmationDialog(); }); // Demande confirmation avant reset
        quitAppButton.setOnClickListener(v -> { dialog.dismiss(); finishAffinity(); }); // Quitte complètement l'application
        closeButton.setOnClickListener(v -> dialog.dismiss()); // Ferme le dialogue des paramètres

        dialog.show(); // Affiche le dialogue
    }

    /**
     * Met à jour l'état local de `notificationsEnabled` et sauvegarde la préférence
     * si la valeur est mise à `false`. Utilisé principalement après un refus de permission.
     *
     * @param isEnabled L'état actuel (généralement `false` lors de l'appel).
     */
    private void updateNotificationSwitchState(boolean isEnabled) {
        if (!isEnabled) {
            notificationsEnabled = false;
            saveNotificationPreference(false);
        }
        // Pas d'action si isEnabled est true ici, car la logique est dans requestNotificationPermission
    }

    /**
     * Sauvegarde la préférence d'activation des notifications dans les SharedPreferences.
     *
     * @param enabled {@code true} si les notifications doivent être activées, {@code false} sinon.
     */
    private void saveNotificationPreference(boolean enabled) {
        if (preferences != null) {
            preferences.edit().putBoolean(NOTIFICATIONS_ENABLED_KEY, enabled).apply();
        }
    }

    /**
     * Charge la préférence d'activation des notifications depuis les SharedPreferences.
     * Met à jour la variable membre `notificationsEnabled`.
     */
    private void loadNotificationPreference() {
        if (preferences != null) {
            notificationsEnabled = preferences.getBoolean(NOTIFICATIONS_ENABLED_KEY, false); // Désactivé par défaut
        } else {
            notificationsEnabled = false;
        }
    }

    /**
     * Sauvegarde la préférence d'activation du son dans les SharedPreferences.
     *
     * @param enabled {@code true} si le son doit être activé, {@code false} sinon.
     */
    private void saveSoundPreference(boolean enabled) {
        if (preferences != null) {
            preferences.edit().putBoolean(SOUND_ENABLED_KEY, enabled).apply();
        }
    }

    /**
     * Charge la préférence d'activation du son depuis les SharedPreferences.
     * Met à jour la variable membre `soundEnabled`.
     */
    private void loadSoundPreference() {
        if (preferences != null) {
            soundEnabled = preferences.getBoolean(SOUND_ENABLED_KEY, true); // Activé par défaut
        } else {
            soundEnabled = true;
        }
    }

    /**
     * Ouvre l'écran des paramètres système de l'application pour permettre à l'utilisateur
     * de gérer manuellement les permissions.
     */
    private void openAppSettings() {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", getPackageName(), null);
        intent.setData(uri);
        try {
            startActivity(intent); // Lance l'intent
        } catch (ActivityNotFoundException e) {
            // Gère le cas où l'intent ne peut pas être résolu
            Toast.makeText(this, "Cannot open settings.", Toast.LENGTH_LONG).show();
        }
    }

    /**
     * Demande la permission d'afficher des notifications si nécessaire (Android 13+).
     * Si la permission est déjà accordée ou non requise (versions antérieures), active
     * les notifications si elles ne l'étaient pas déjà.
     */
    private void requestNotificationPermission() {
        // Si Android 13 (Tiramisu) ou supérieur
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // Vérifie si la permission n'est PAS déjà accordée
            if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
                // Lance la demande de permission via le launcher enregistré
                requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS);
            } else {
                // Permission déjà accordée, active les notifications si elles étaient désactivées
                if (!notificationsEnabled) {
                    notificationsEnabled = true;
                    saveNotificationPreference(true);
                    Toast.makeText(this, R.string.notifications_enabled, Toast.LENGTH_SHORT).show();
                    schedulePeriodicNotifications();
                }
            }
        } else {
            // Pour les versions antérieures à Android 13, la permission n'est pas requise
            // Active simplement les notifications si elles étaient désactivées
            if (!notificationsEnabled) {
                notificationsEnabled = true;
                saveNotificationPreference(true);
                Toast.makeText(this, R.string.notifications_enabled, Toast.LENGTH_SHORT).show();
                schedulePeriodicNotifications();
            }
        }
    }

    /**
     * Affiche une notification en utilisant {@link NotificationHelper}.
     * Ne fait rien si les notifications sont désactivées globalement.
     *
     * @param context        Contexte.
     * @param title          Titre de la notification.
     * @param message        Message de la notification.
     * @param notificationId ID unique de la notification.
     */
    private void showNotification(Context context, String title, String message, int notificationId) {
        if (!notificationsEnabled) {
            return; // Ne pas afficher si désactivé
        }
        // Délègue l'affichage à NotificationHelper
        NotificationHelper.showNotification(context, title, message, notificationId);
    }

    /**
     * Planifie une tâche périodique en arrière-plan ({@link NotificationWorker})
     * en utilisant {@link WorkManager} pour vérifier et afficher des notifications.
     * Utilise une politique {@code KEEP} pour éviter de replanifier si un travail identique est déjà programmé.
     */
    private void schedulePeriodicNotifications() {
        // Crée la requête de travail périodique
        PeriodicWorkRequest periodicWorkRequest =
                new PeriodicWorkRequest.Builder(NotificationWorker.class,
                        WORKER_REPEAT_INTERVAL_HOURS, TimeUnit.HOURS) // Répète toutes les X heures
                        .build();
        // Met en file d'attente le travail avec un tag unique et une politique KEEP
        WorkManager.getInstance(getApplicationContext()).enqueueUniquePeriodicWork(
                PERIODIC_NOTIFICATION_WORK_TAG, // Tag pour identifier/annuler le travail
                ExistingPeriodicWorkPolicy.KEEP, // Garde le travail existant s'il y en a un
                periodicWorkRequest
        );
    }

    /**
     * Annule la tâche périodique de notification en arrière-plan si elle était planifiée.
     */
    private void cancelPeriodicNotifications() {
        // Annule le travail en utilisant son tag unique
        WorkManager.getInstance(getApplicationContext()).cancelUniqueWork(PERIODIC_NOTIFICATION_WORK_TAG);
    }

    /**
     * Affiche une notification de "succès" lorsqu'une tuile significative (ex: 2048) est atteinte.
     *
     * @param tileValue La valeur de la tuile atteinte.
     */
    private void showAchievementNotification(int tileValue) {
        if (!notificationsEnabled) return; // Vérifie si les notifications sont activées
        String title = getString(R.string.notification_title_achievement);
        String message = getString(R.string.notification_text_achievement, tileValue);
        // Affiche la notification via la méthode wrapper
        showNotification(this, title, message, 1); // Utilise ID 1 pour ce type de notif
    }

    /**
     * Prépare et lance une intention pour partager les statistiques de jeu actuelles
     * via les applications disponibles sur l'appareil (email, réseaux sociaux, etc.).
     */
    private void shareStats() {
        if (gameStats == null) {
            Toast.makeText(this, "Stats not available.", Toast.LENGTH_SHORT).show();
            return;
        }

        // Construit le message à partager avec les statistiques formatées
        String shareBody = getString(R.string.share_stats_body,
                gameStats.getOverallHighScore(),
                gameStats.getHighestTile(),
                gameStats.getNumberOfTimesObjectiveReached(),
                gameStats.getTotalGamesStarted(),
                GameStats.formatTime(gameStats.getTotalPlayTimeMs()), // Formate le temps
                gameStats.getTotalMoves()
        );

        // Crée l'intention de partage
        Intent shareIntent = new Intent(Intent.ACTION_SEND);
        shareIntent.setType("text/plain"); // Type de contenu texte
        shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_stats_subject)); // Sujet (pour email, etc.)
        shareIntent.putExtra(Intent.EXTRA_TEXT, shareBody); // Corps du message

        // Lance le sélecteur d'applications de partage
        try {
            startActivity(Intent.createChooser(shareIntent, getString(R.string.share_stats_title)));
        } catch (ActivityNotFoundException e) {
            // Gère le cas où aucune application de partage n'est installée
            Toast.makeText(this, "No sharing app available.", Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * Affiche un dialogue demandant confirmation avant de réinitialiser toutes les statistiques de jeu.
     */
    private void showResetStatsConfirmationDialog() {
        new AlertDialog.Builder(this)
                .setTitle(R.string.reset_stats_confirm_title)
                .setMessage(R.string.reset_stats_confirm_message)
                .setIcon(android.R.drawable.ic_dialog_alert) // Icône d'avertissement standard
                .setPositiveButton(R.string.confirm, (dialog, which) -> { // Bouton Confirmer
                    resetStatistics(); // Appelle la méthode de réinitialisation
                    dialog.dismiss();
                })
                .setNegativeButton(R.string.cancel, (dialog, which) -> { // Bouton Annuler
                    dialog.dismiss(); // Ferme simplement le dialogue
                })
                .setCancelable(false) // Non annulable en cliquant à l'extérieur
                .show();
    }

    /**
     * Réinitialise toutes les statistiques de jeu en appelant {@link GameStats#resetStats()}.
     * Met également à jour le meilleur score affiché dans le jeu en cours et actualise
     * le panneau des statistiques s'il est visible. Affiche un message de confirmation.
     */
    private void resetStatistics() {
        if (gameStats != null) {
            gameStats.resetStats(); // Réinitialise les données dans GameStats (et sauvegarde)
            // Met à jour le meilleur score dans l'instance 'game' actuelle
            if(game != null){
                game.setHighestScore(gameStats.getOverallHighScore()); // Le high score est maintenant 0
                updateScores(); // Met à jour l'affichage des scores
            }
            Toast.makeText(this, R.string.stats_reset_confirmation, Toast.LENGTH_SHORT).show(); // Confirmation
            // Met à jour le panneau des stats s'il est affiché
            if (statisticsVisible && inflatedStatsView != null) {
                updateStatisticsTextViews();
            }
        }
    }

    /**
     * Affiche un dialogue "À Propos" contenant des informations sur l'application
     * et un lien cliquable vers un site web.
     */
    private void showAboutDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        LayoutInflater inflater = getLayoutInflater();
        View dialogView = inflater.inflate(R.layout.dialog_about, null); // Charge le layout
        builder.setView(dialogView);
        builder.setCancelable(true); // Annulable

        TextView websiteLinkTextView = dialogView.findViewById(R.id.websiteLinkTextView); // Lien texte
        Button okButton = dialogView.findViewById(R.id.dialogOkButtonAbout); // Bouton OK

        final AlertDialog dialog = builder.create();

        // Rend le lien cliquable pour ouvrir un navigateur web
        websiteLinkTextView.setOnClickListener(v -> {
            String url = getString(R.string.about_website_url); // URL depuis les ressources
            Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
            try {
                startActivity(browserIntent); // Tente d'ouvrir le navigateur
            } catch (ActivityNotFoundException e) {
                Toast.makeText(this, "No web browser found.", Toast.LENGTH_SHORT).show(); // Erreur si pas de navigateur
            }
        });
        // Bouton OK pour fermer le dialogue
        okButton.setOnClickListener(v -> dialog.dismiss());
        dialog.show(); // Affiche le dialogue
    }

    /**
     * Affiche ou masque le panneau des statistiques.
     * Charge la vue à partir du {@link ViewStub} lors de la première ouverture.
     * Met à jour le contenu des TextViews de statistiques lorsque le panneau est affiché.
     * Gère également la visibilité du bouton Multijoueur pour éviter les superpositions.
     */
    private void toggleStatistics() {
        statisticsVisible = !statisticsVisible; // Inverse l'état de visibilité

        if (statisticsVisible) {
            // Si on veut afficher les stats
            if (inflatedStatsView == null) {
                // Si la vue n'a pas encore été chargée (première fois)
                try {
                    inflatedStatsView = statisticsViewStub.inflate(); // Charge la vue depuis le ViewStub
                    // Trouve le bouton Retour dans la vue chargée et configure son listener
                    Button backButton = inflatedStatsView.findViewById(R.id.backButton);
                    if (backButton != null) {
                        backButton.setOnClickListener(v -> toggleStatistics()); // Le bouton Retour rappelle cette même méthode
                    }
                } catch (Exception e) {
                    // Gère une erreur potentielle lors du chargement de la vue
                    statisticsVisible = false; // Revient à l'état masqué
                    Toast.makeText(this, "Error displaying stats.", Toast.LENGTH_SHORT).show();
                    return;
                }
            }
            // Si la vue est chargée (ou vient de l'être)
            if (inflatedStatsView != null) {
                updateStatisticsTextViews(); // Met à jour les données affichées
                inflatedStatsView.setVisibility(View.VISIBLE); // Rend la vue visible
            }
            multiplayerButton.setVisibility(View.GONE); // Masque le bouton Multijoueur
        } else {
            // Si on veut masquer les stats
            if (inflatedStatsView != null) {
                inflatedStatsView.setVisibility(View.GONE); // Masque la vue des stats
            }
            multiplayerButton.setVisibility(View.VISIBLE); // Réaffiche le bouton Multijoueur
        }
    }

    /**
     * Met à jour le contenu textuel de tous les TextViews dans le panneau des statistiques
     * en utilisant les données actuelles de l'instance {@link GameStats}.
     * Formate les pourcentages et les durées de manière appropriée.
     */
    @SuppressLint("StringFormatInvalid") // Supprime le warning pour le format de temps multi, même si corrigé.
    private void updateStatisticsTextViews() {
        // Vérifie si la vue et les stats sont disponibles
        if (inflatedStatsView == null || gameStats == null) {
            return;
        }

        // Récupère les références de tous les TextViews de statistiques (peut être optimisé)
        TextView highScoreStatsLabel = inflatedStatsView.findViewById(R.id.high_score_stats_label);
        TextView totalGamesPlayedLabel = inflatedStatsView.findViewById(R.id.total_games_played_label);
        TextView totalGamesStartedLabel = inflatedStatsView.findViewById(R.id.total_games_started_label);
        TextView winPercentageLabel = inflatedStatsView.findViewById(R.id.win_percentage_label);
        TextView totalPlayTimeLabel = inflatedStatsView.findViewById(R.id.total_play_time_label);
        TextView totalMovesLabel = inflatedStatsView.findViewById(R.id.total_moves_label);
        TextView currentMovesLabel = inflatedStatsView.findViewById(R.id.current_moves_label);
        TextView currentGameTimeLabel = inflatedStatsView.findViewById(R.id.current_game_time_label);
        TextView averageGameTimeLabel = inflatedStatsView.findViewById(R.id.average_game_time_label);
        TextView bestWinningTimeLabel = inflatedStatsView.findViewById(R.id.best_winning_time_label);
        TextView worstWinningTimeLabel = inflatedStatsView.findViewById(R.id.worst_winning_time_label);
        TextView totalMergesLabel = inflatedStatsView.findViewById(R.id.total_merges_label);
        TextView highestTileLabel = inflatedStatsView.findViewById(R.id.highest_tile_label);
        TextView objectiveReachedLabel = inflatedStatsView.findViewById(R.id.number_of_time_objective_reached_label);
        TextView mergesThisGameLabel = inflatedStatsView.findViewById(R.id.merges_this_game);
        TextView multiplayerGamesWonLabel = inflatedStatsView.findViewById(R.id.multiplayer_games_won_label);
        TextView multiplayerGamesPlayedLabel = inflatedStatsView.findViewById(R.id.multiplayer_games_played_label);
        TextView multiplayerWinRateLabel = inflatedStatsView.findViewById(R.id.multiplayer_win_rate_label);
        TextView multiplayerBestWinningStreakLabel = inflatedStatsView.findViewById(R.id.multiplayer_best_winning_streak_label);
        TextView multiplayerAverageScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_average_score_label);
        TextView averageTimePerGameMultiLabel = inflatedStatsView.findViewById(R.id.average_time_per_game_multi_label); // Corrigé
        TextView totalMultiplayerLossesLabel = inflatedStatsView.findViewById(R.id.total_multiplayer_losses_label);
        TextView multiplayerHighScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_high_score_label);

        // Met à jour le texte de chaque TextView avec les données formatées
        if (highScoreStatsLabel != null) highScoreStatsLabel.setText(getString(R.string.high_score_stats, gameStats.getOverallHighScore()));
        if (totalGamesPlayedLabel != null) totalGamesPlayedLabel.setText(getString(R.string.total_games_played, gameStats.getTotalGamesPlayed()));
        if (totalGamesStartedLabel != null) totalGamesStartedLabel.setText(getString(R.string.total_games_started, gameStats.getTotalGamesStarted()));
        if (totalMovesLabel != null) totalMovesLabel.setText(getString(R.string.total_moves, gameStats.getTotalMoves()));
        if (currentMovesLabel != null) currentMovesLabel.setText(getString(R.string.current_moves, gameStats.getCurrentMoves()));
        if (mergesThisGameLabel != null) mergesThisGameLabel.setText(getString(R.string.merges_this_game_label, gameStats.getMergesThisGame()));
        if (totalMergesLabel != null) totalMergesLabel.setText(getString(R.string.total_merges, gameStats.getTotalMerges()));
        if (highestTileLabel != null) highestTileLabel.setText(getString(R.string.highest_tile, gameStats.getHighestTile()));
        if (objectiveReachedLabel != null) objectiveReachedLabel.setText(getString(R.string.number_of_time_objective_reached, gameStats.getNumberOfTimesObjectiveReached()));

        // Calcule et formate le pourcentage de victoires solo
        if (winPercentageLabel != null) {
            String winPercentage = (gameStats.getTotalGamesStarted() > 0)
                    ? String.format(java.util.Locale.US, "%.1f%%", ((double) gameStats.getNumberOfTimesObjectiveReached() / gameStats.getTotalGamesStarted()) * 100)
                    : "N/A"; // Affiche N/A si aucune partie démarrée
            winPercentageLabel.setText(getString(R.string.win_percentage, winPercentage));
        }
        // Calcule et formate le pourcentage de victoires multi
        if (multiplayerWinRateLabel != null) {
            String multiplayerWinRate = (gameStats.getMultiplayerGamesPlayed() > 0)
                    ? String.format(java.util.Locale.US, "%.1f%%", ((double) gameStats.getMultiplayerGamesWon() / gameStats.getMultiplayerGamesPlayed()) * 100)
                    : "N/A"; // Affiche N/A si aucune partie jouée
            multiplayerWinRateLabel.setText(getString(R.string.multiplayer_win_rate, multiplayerWinRate));
        }

        // Met à jour les labels de temps (formatés par GameStats.formatTime)
        if (totalPlayTimeLabel != null) totalPlayTimeLabel.setText(getString(R.string.total_play_time, GameStats.formatTime(gameStats.getTotalPlayTimeMs())));
        if (currentGameTimeLabel != null) {
            long currentDurationMs = 0;
            // Calcule la durée de la partie en cours si elle est active
            if (game != null && currentGameState == GameFlowState.PLAYING && gameStats.getCurrentGameStartTimeMs() > 0) {
                currentDurationMs = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
            }
            currentGameTimeLabel.setText(getString(R.string.current_game_time, GameStats.formatTime(currentDurationMs)));
        }
        if (averageGameTimeLabel != null) averageGameTimeLabel.setText(getString(R.string.average_time_per_game, GameStats.formatTime(gameStats.getAverageGameTimeMs())));
        if (bestWinningTimeLabel != null) bestWinningTimeLabel.setText(getString(R.string.best_winning_time, (gameStats.getBestWinningTimeMs() != Long.MAX_VALUE) ? GameStats.formatTime(gameStats.getBestWinningTimeMs()) : "N/A"));
        if (worstWinningTimeLabel != null) worstWinningTimeLabel.setText(getString(R.string.worst_winning_time, (gameStats.getWorstWinningTimeMs() != 0) ? GameStats.formatTime(gameStats.getWorstWinningTimeMs()) : "N/A"));

        // Met à jour les statistiques multijoueur
        if (multiplayerGamesWonLabel != null) multiplayerGamesWonLabel.setText(getString(R.string.multiplayer_games_won, gameStats.getMultiplayerGamesWon()));
        if (multiplayerGamesPlayedLabel != null) multiplayerGamesPlayedLabel.setText(getString(R.string.multiplayer_games_played, gameStats.getMultiplayerGamesPlayed()));
        if (multiplayerBestWinningStreakLabel != null) multiplayerBestWinningStreakLabel.setText(getString(R.string.multiplayer_best_winning_streak, gameStats.getMultiplayerBestWinningStreak()));
        if (multiplayerAverageScoreLabel != null) multiplayerAverageScoreLabel.setText(getString(R.string.multiplayer_average_score, gameStats.getMultiplayerAverageScore()));
        if (totalMultiplayerLossesLabel != null) totalMultiplayerLossesLabel.setText(getString(R.string.total_multiplayer_losses, gameStats.getTotalMultiplayerLosses()));
        if (multiplayerHighScoreLabel != null) multiplayerHighScoreLabel.setText(getString(R.string.multiplayer_high_score, gameStats.getMultiplayerHighestScore()));
        if (averageTimePerGameMultiLabel != null) {
            // Utilise le bon ID de string pour le temps moyen multi (supposons R.string.average_time_per_game_label)
            try {
                averageTimePerGameMultiLabel.setText(getString(R.string.average_time_per_game_label, GameStats.formatTime(gameStats.getMultiplayerAverageTimeMs())));
            } catch (Exception e) {
                // Fallback au cas où l'ID de string serait incorrect ou manquant
                averageTimePerGameMultiLabel.setText(R.string.average_time_per_game_label);
            }
        }
    }

    /**
     * Lance l'activité {@link MultiplayerActivity} pour démarrer ou rejoindre une partie multijoueur.
     */
    private void showMultiplayerScreen() {
        Intent intent = new Intent(this, MultiplayerActivity.class); // Crée l'intent
        try {
            startActivity(intent); // Lance l'activité
        } catch (ActivityNotFoundException e) {
            // Gère le cas où l'activité ne peut pas être démarrée
            Toast.makeText(this, "Error: Multiplayer feature unavailable.", Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * Sauvegarde l'état actuel du jeu (plateau et score) et le meilleur score
     * dans les SharedPreferences.
     */
    private void saveGame() {
        if (preferences == null) {
            return; // Ne peut pas sauvegarder sans SharedPreferences
        }
        SharedPreferences.Editor editor = preferences.edit(); // Obtient l'éditeur
        if (game != null) {
            // Sérialise l'état du jeu (en utilisant game.toString()) et le meilleur score
            editor.putString(GAME_STATE_KEY, game.toString());
            editor.putInt(HIGH_SCORE_KEY, game.getHighestScore());
        } else {
            // S'il n'y a pas de jeu en cours, supprime l'état sauvegardé
            editor.remove(GAME_STATE_KEY);
        }
        editor.apply(); // Applique les changements de manière asynchrone
    }

    /**
     * Charge l'état du jeu sauvegardé depuis les SharedPreferences.
     * Désérialise l'état du plateau et récupère le meilleur score.
     * Met à jour l'instance {@link #game} et {@link #currentGameState} en conséquence.
     * S'assure que le meilleur score est synchronisé entre l'état chargé et les statistiques globales.
     */
    private void loadGame() {
        if (preferences == null) {
            game = null; // Ne peut pas charger sans SharedPreferences
            return;
        }
        // Récupère l'état sérialisé et le high score depuis les prefs
        String gameStateString = preferences.getString(GAME_STATE_KEY, null);
        int loadedHighScore = preferences.getInt(HIGH_SCORE_KEY, 0);

        // S'assure que gameStats est initialisé pour récupérer le high score global
        if (gameStats == null) {
            gameStats = new GameStats(this);
        }
        // Prend le maximum entre le high score chargé et celui des stats globales
        int authoritativeHighScore = Math.max(loadedHighScore, gameStats.getOverallHighScore());
        // Met à jour les stats globales si le high score chargé était plus élevé
        if (authoritativeHighScore > gameStats.getOverallHighScore()) {
            gameStats.setHighestScore(authoritativeHighScore);
        }

        Game loadedGame = null;
        // Tente de désérialiser l'état du jeu si une chaîne a été trouvée
        if (gameStateString != null) {
            loadedGame = Game.deserialize(gameStateString);
        }

        // Si le chargement/désérialisation a réussi
        if (loadedGame != null) {
            game = loadedGame; // Utilise le jeu chargé
            // Définit le meilleur score correct
            game.setHighestScore(authoritativeHighScore);
            // Détermine l'état du flux de jeu en fonction de l'état chargé
            if (game.isGameOver()) {
                currentGameState = GameFlowState.GAME_OVER;
            } else if (game.isGameWon()) {
                // Si gagné, on met l'état comme si le dialogue avait déjà été montré pour éviter de le remontrer au chargement
                currentGameState = GameFlowState.WON_DIALOG_SHOWN;
            } else {
                currentGameState = GameFlowState.PLAYING;
            }
        } else {
            // Si échec du chargement, initialise à null (startNewGame sera appelé)
            game = null;
            currentGameState = GameFlowState.PLAYING;
        }
    }

    /**
     * Sauvegarde le timestamp actuel dans les SharedPreferences comme 'dernière fois jouée'.
     * Utilisé par le {@link NotificationWorker} pour détecter l'inactivité.
     */
    private void saveLastPlayedTime() {
        if (preferences != null) {
            long now = System.currentTimeMillis(); // Heure actuelle
            preferences.edit().putLong(LAST_PLAYED_TIME_KEY, now).apply(); // Sauvegarde
        }
    }
}