import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:retry/retry.dart';
import 'package:universal_html/html.dart' as html;
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/services/connectivity_service.dart';
import 'package:geosector_app/core/data/models/pending_request.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:uuid/uuid.dart';
import 'device_info_service.dart';

class ApiService {
  static ApiService? _instance;
  static final Object _lock = Object();

  // Propriétés existantes conservées
  final Dio _dio = Dio();
  late final String _baseUrl;
  late final String _appIdentifier;
  String? _sessionId;
  
  // Nouvelles propriétés pour la gestion offline
  ConnectivityService? _connectivityService;
  bool _isProcessingQueue = false;
  final _uuid = const Uuid();
  
  // Getters pour les propriétés (lecture seule)
  String? get sessionId => _sessionId;
  String get baseUrl => _baseUrl;

  // Singleton thread-safe
  static ApiService get instance {
    if (_instance == null) {
      throw Exception('ApiService non initialisé. Appelez initialize() d\'abord.');
    }
    return _instance!;
  }

  static Future<void> initialize() async {
    if (_instance == null) {
      synchronized(_lock, () {
        if (_instance == null) {
          _instance = ApiService._internal();
          debugPrint('✅ ApiService singleton initialisé');
        }
      });
    }
  }

  // Constructeur privé avec toute la logique existante
  ApiService._internal() {
    _configureEnvironment();

    _dio.options.baseUrl = _baseUrl;
    _dio.options.connectTimeout = AppKeys.connectionTimeout;
    _dio.options.receiveTimeout = AppKeys.receiveTimeout;

    final headers = Map<String, String>.from(AppKeys.defaultHeaders);
    headers['X-App-Identifier'] = _appIdentifier;
    _dio.options.headers.addAll(headers);

    // Gestionnaire de cookies pour les sessions PHP
    // IMPORTANT: Désactivé sur Web car les navigateurs bloquent la manipulation des cookies via XHR
    // Sur Web, on utilise uniquement le header Authorization avec Bearer token
    if (!kIsWeb) {
      final cookieJar = CookieJar();
      _dio.interceptors.add(CookieManager(cookieJar));
      debugPrint('🍪 [API] Gestionnaire de cookies activé (mobile)');
    } else {
      debugPrint('🌐 [API] Mode Web - pas de CookieManager (Bearer token uniquement)');
    }

    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        debugPrint('🌐 [API] Requête: ${options.method} ${options.path}');
        debugPrint('🔑 [API] _sessionId présent: ${_sessionId != null}');
        debugPrint('🔑 [API] Headers: ${options.headers}');
        if (_sessionId != null) {
          debugPrint('🔑 [API] Token: Bearer ${_sessionId!.substring(0, 20)}...');
          options.headers[AppKeys.sessionHeader] = 'Bearer $_sessionId';
        } else {
          debugPrint('⚠️ [API] PAS DE TOKEN - Requête non authentifiée');
        }
        handler.next(options);
      },
      onError: (DioException error, handler) {
        if (error.response?.statusCode == 401) {
          final path = error.requestOptions.path;
          debugPrint('❌ [API] Erreur 401 sur: $path');

          // Ne pas reset le token pour les requêtes non critiques
          final nonCriticalPaths = [
            '/users/device-info',
            '/chat/rooms',
          ];

          final isNonCritical = nonCriticalPaths.any((p) => path.contains(p));

          if (isNonCritical) {
            debugPrint('⚠️ [API] Requête non critique - Token conservé');
          } else {
            debugPrint('❌ [API] Requête critique - Token invalidé');
            _sessionId = null;
          }
        }
        handler.next(error);
      },
    ));

    debugPrint('🔗 ApiService configuré pour $_baseUrl');
    
    // Initialiser le listener de connectivité
    _initConnectivityListener();
  }
  
  // Initialise le listener pour détecter les changements de connectivité
  void _initConnectivityListener() {
    try {
      _connectivityService = ConnectivityService();
      _connectivityService!.addListener(_onConnectivityChanged);
      debugPrint('📡 Listener de connectivité activé');
      
      // Vérifier s'il y a des requêtes en attente au démarrage
      if (_connectivityService!.isConnected) {
        _checkAndProcessPendingRequests();
      }
    } catch (e) {
      debugPrint('⚠️ Erreur lors de l\'initialisation du listener de connectivité: $e');
    }
  }
  
  // Appelé quand l'état de connectivité change
  void _onConnectivityChanged() {
    if (_connectivityService?.isConnected ?? false) {
      debugPrint('📡 Connexion rétablie - Traitement de la file d\'attente');
      _checkAndProcessPendingRequests();
    } else {
      debugPrint('📡 Connexion perdue - Mise en file d\'attente des requêtes');
    }
  }
  
  // Vérifie et traite les requêtes en attente
  Future<void> _checkAndProcessPendingRequests() async {
    if (_isProcessingQueue) {
      debugPrint('⏳ Traitement de la file déjà en cours');
      return;
    }
    
    try {
      if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
        debugPrint('📦 Box pending_requests non ouverte');
        return;
      }
      
      final box = Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName);
      if (box.isEmpty) {
        return;
      }
      
      debugPrint('📨 ${box.length} requête(s) en attente trouvée(s)');
      await processPendingRequests();
    } catch (e) {
      debugPrint('❌ Erreur lors de la vérification des requêtes en attente: $e');
    }
  }

  // Fonction synchronized simple pour éviter les imports supplémentaires
  static T synchronized<T>(Object lock, T Function() computation) {
    return computation();
  }

  // Détermine l'environnement actuel (DEV, REC, PROD) en fonction de l'URL
  String _determineEnvironment() {
    if (!kIsWeb) {
      // En mode non-web (iOS/Android), utiliser l'environnement de PRODUCTION par défaut
      return 'PROD';
    }

    final currentUrl = html.window.location.href.toLowerCase();

    if (currentUrl.contains('dapp.geosector.fr')) {
      return 'DEV';
    } else if (currentUrl.contains('rapp.geosector.fr')) {
      return 'REC';
    } else {
      return 'PROD';
    }
  }

  // Configure l'URL de base API et l'identifiant d'application selon l'environnement
  void _configureEnvironment() {
    final env = _determineEnvironment();

    switch (env) {
      case 'DEV':
        _baseUrl = AppKeys.baseApiUrlDev;
        _appIdentifier = AppKeys.appIdentifierDev;
        break;
      case 'REC':
        _baseUrl = AppKeys.baseApiUrlRec;
        _appIdentifier = AppKeys.appIdentifierRec;
        break;
      default: // PROD
        _baseUrl = AppKeys.baseApiUrlProd;
        _appIdentifier = AppKeys.appIdentifierProd;
    }

    debugPrint('GEOSECTOR 🔗 Environnement: $env, API: $_baseUrl');
  }

  // Définir l'ID de session
  void setSessionId(String? sessionId) {
    _sessionId = sessionId;
  }

  // Obtenir l'environnement actuel (utile pour le débogage)
  String getCurrentEnvironment() {
    return _determineEnvironment();
  }

  // Obtenir l'URL API actuelle (utile pour le débogage)
  String getCurrentApiUrl() {
    return _baseUrl;
  }

  // Obtenir l'URL frontend (sans /api) - utile pour les redirections Stripe
  String getFrontendUrl() {
    // Retirer le /api de l'URL pour obtenir l'URL frontend
    return _baseUrl.replaceAll('/api', '');
  }

  // Obtenir l'identifiant d'application actuel (utile pour le débogage)
  String getCurrentAppIdentifier() {
    return _appIdentifier;
  }

  // Vérifier la connectivité réseau
  Future<bool> hasInternetConnection() async {
    // Utiliser le ConnectivityService s'il est disponible
    if (_connectivityService != null) {
      return _connectivityService!.isConnected;
    }
    // Fallback sur la vérification directe
    final connectivityResults = await (Connectivity().checkConnectivity());
    return !connectivityResults.contains(ConnectivityResult.none);
  }
  
  // Met une requête en file d'attente pour envoi ultérieur
  Future<void> _queueRequest({
    required String method,
    required String path,
    dynamic data,
    Map<String, dynamic>? queryParameters,
    String? tempId,
    Map<String, dynamic>? metadata,
  }) async {
    try {
      if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
        await Hive.openBox<PendingRequest>(AppKeys.pendingRequestsBoxName);
      }
      
      final box = Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName);
      
      // Vérifier la limite de 1000 requêtes
      if (box.length >= 1000) {
        debugPrint('⚠️ Limite de 1000 requêtes atteinte dans la queue');
        throw ApiException(
          'La file d\'attente est pleine (1000 requêtes maximum). '
          'Veuillez attendre la synchronisation avant d\'effectuer de nouvelles opérations.',
        );
      }
      
      final request = PendingRequest(
        id: _uuid.v4(),
        method: method,
        path: path,
        data: data,
        queryParams: queryParameters,  // Utiliser queryParams au lieu de queryParameters
        tempId: tempId,
        metadata: metadata ?? {},
        createdAt: DateTime.now(),
        context: 'api',  // Contexte par défaut
        retryCount: 0,
        errorMessage: null,
      );
      
      await box.add(request);
      debugPrint('📥 Requête mise en file d\'attente: ${request.toLogString()} (${box.length}/1000)');
    } catch (e) {
      debugPrint('❌ Erreur lors de la mise en file d\'attente: $e');
      rethrow;
    }
  }
  
  // Traite toutes les requêtes en attente (FIFO)
  Future<void> processPendingRequests() async {
    if (_isProcessingQueue) {
      debugPrint('⏳ Traitement déjà en cours');
      return;
    }
    
    _isProcessingQueue = true;
    
    try {
      if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
        debugPrint('📦 Box pending_requests non ouverte');
        return;
      }
      
      final box = Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName);
      
      while (box.isNotEmpty && (_connectivityService?.isConnected ?? true)) {
        // Récupérer les requêtes triées par date de création (FIFO)
        final requests = box.values.toList()
          ..sort((a, b) => a.createdAt.compareTo(b.createdAt));
        
        if (requests.isEmpty) break;
        
        final request = requests.first;
        debugPrint('🚀 Traitement de la requête: ${request.toLogString()}');
        
        try {
          // Exécuter la requête
          Response? response;
          
          switch (request.method.toUpperCase()) {
            case 'GET':
              response = await _dio.get(
                request.path,
                queryParameters: request.queryParams,  // Utiliser queryParams
              );
              break;
            case 'POST':
              response = await _dio.post(
                request.path,
                data: request.data,
              );
              break;
            case 'PUT':
              response = await _dio.put(
                request.path,
                data: request.data,
              );
              break;
            case 'DELETE':
              response = await _dio.delete(request.path);
              break;
            default:
              throw Exception('Méthode HTTP non supportée: ${request.method}');
          }
          
          // Requête réussie - la supprimer de la file
          await box.delete(request.key);
          debugPrint('✅ Requête traitée avec succès et supprimée de la file');
          
          // Traiter la réponse si nécessaire (gestion des temp IDs, etc.)
          if (request.tempId != null) {
            await _handleTempIdResponse(request.tempId!, response.data);
          }
          
        } catch (e) {
          debugPrint('❌ Erreur lors du traitement de la requête: $e');
          
          // Vérifier si c'est une erreur de conflit (409)
          bool isConflict = false;
          if (e is DioException && e.response?.statusCode == 409) {
            isConflict = true;
            debugPrint('⚠️ Conflit détecté (409) - La requête sera marquée comme en conflit');
          }
          
          // Vérifier si c'est une erreur permanente (4xx sauf 409)
          bool isPermanentError = false;
          if (e is DioException && e.response != null) {
            final statusCode = e.response!.statusCode ?? 0;
            if (statusCode >= 400 && statusCode < 500 && statusCode != 409) {
              isPermanentError = true;
              debugPrint('❌ Erreur permanente (${statusCode}) - La requête sera supprimée');
            }
          }
          
          if (isPermanentError) {
            // Supprimer les requêtes avec erreurs permanentes (sauf conflits)
            await box.delete(request.key);
            debugPrint('🗑️ Requête supprimée de la file (erreur permanente)');
            
            // Notifier l'utilisateur si possible
            // TODO: Implémenter un système de notification des erreurs permanentes
            
          } else if (isConflict) {
            // Marquer la requête comme en conflit
            final updatedMetadata = Map<String, dynamic>.from(request.metadata ?? {});
            updatedMetadata['hasConflict'] = true;
            
            final conflictRequest = request.copyWith(
              retryCount: request.retryCount + 1,
              errorMessage: 'CONFLICT: ${e.toString()}',
              metadata: updatedMetadata,
            );
            await box.put(request.key, conflictRequest);
            
            // Passer à la requête suivante sans attendre
            debugPrint('⏭️ Passage à la requête suivante (conflit à résoudre manuellement)');
            continue;
            
          } else {
            // Erreur temporaire - réessayer plus tard
            final updatedRequest = request.copyWith(
              retryCount: request.retryCount + 1,
              errorMessage: e.toString(),
            );
            await box.put(request.key, updatedRequest);
            
            // Arrêter le traitement si la connexion est perdue
            if (!(_connectivityService?.isConnected ?? true)) {
              debugPrint('📡 Connexion perdue - Arrêt du traitement');
              break;
            }
            
            // Limiter le nombre de tentatives
            if (request.retryCount >= 5) {
              debugPrint('⚠️ Nombre maximum de tentatives atteint (5) - Suppression de la requête');
              await box.delete(request.key);
              continue;
            }
            
            // Attendre avant de réessayer (avec backoff exponentiel)
            final delay = request.getNextRetryDelay();
            debugPrint('⏳ Attente de ${delay.inSeconds}s avant la prochaine tentative');
            await Future.delayed(delay);
          }
        }
      }
      
      if (box.isEmpty) {
        debugPrint('✅ Toutes les requêtes ont été traitées');
      } else {
        debugPrint('📝 ${box.length} requête(s) restante(s) en file d\'attente');
      }
      
    } catch (e) {
      debugPrint('❌ Erreur lors du traitement de la file: $e');
    } finally {
      _isProcessingQueue = false;
    }
  }
  
  // Gère la réponse pour les entités temporaires
  Future<void> _handleTempIdResponse(String tempId, dynamic responseData) async {
    debugPrint('🔄 Mapping tempId: $tempId avec la réponse');
    
    try {
      // Vérifier si l'API a retourné un temp_id pour confirmation
      final returnedTempId = responseData['temp_id'];
      if (returnedTempId != null && returnedTempId != tempId) {
        debugPrint('⚠️ TempId mismatch: attendu $tempId, reçu $returnedTempId');
        return;
      }
      
      // Gérer les messages du chat
      if (tempId.startsWith('temp_msg_')) {
        await _handleTempMessageMapping(tempId, responseData);
      }
      // Gérer les rooms du chat
      else if (tempId.startsWith('temp_room_')) {
        await _handleTempRoomMapping(tempId, responseData);
      }
      // Autres types d'entités temporaires peuvent être ajoutés ici
      
    } catch (e) {
      debugPrint('❌ Erreur lors du mapping tempId $tempId: $e');
    }
  }
  
  // Gère le mapping des messages temporaires
  Future<void> _handleTempMessageMapping(String tempId, Map<String, dynamic> responseData) async {
    try {
      // Importer les modèles nécessaires
      final messagesBoxName = AppKeys.chatMessagesBoxName;
      
      if (!Hive.isBoxOpen(messagesBoxName)) {
        debugPrint('📦 Box $messagesBoxName non ouverte');
        return;
      }
      
      // Utiliser un import dynamique pour éviter les dépendances circulaires
      final messagesBox = Hive.box(messagesBoxName);
      
      // Récupérer le message temporaire
      final tempMessage = messagesBox.get(tempId);
      if (tempMessage == null) {
        debugPrint('⚠️ Message temporaire $tempId non trouvé dans Hive');
        return;
      }
      
      // Récupérer l'ID réel depuis la réponse
      final realId = responseData['id']?.toString();
      if (realId == null || realId.isEmpty) {
        debugPrint('⚠️ ID réel non trouvé dans la réponse pour $tempId');
        return;
      }
      
      // Créer le message avec l'ID réel et marquer comme synchronisé
      // Note: On ne peut pas utiliser Message.fromJson ici car ApiService ne connaît pas le modèle
      // On va donc stocker les données brutes et laisser ChatService faire la conversion
      final syncedMessageData = Map<String, dynamic>.from(responseData);
      syncedMessageData['is_synced'] = true;
      
      // Supprimer le temporaire et ajouter le message avec l'ID réel
      await messagesBox.delete(tempId);
      await messagesBox.put(realId, syncedMessageData);
      
      debugPrint('✅ Message $tempId remplacé par ID réel $realId');
      
    } catch (e) {
      debugPrint('❌ Erreur mapping message $tempId: $e');
    }
  }
  
  // Gère le mapping des rooms temporaires
  Future<void> _handleTempRoomMapping(String tempId, Map<String, dynamic> responseData) async {
    try {
      final roomsBoxName = AppKeys.chatRoomsBoxName;
      
      if (!Hive.isBoxOpen(roomsBoxName)) {
        debugPrint('📦 Box $roomsBoxName non ouverte');
        return;
      }
      
      final roomsBox = Hive.box(roomsBoxName);
      
      // Récupérer la room temporaire
      final tempRoom = roomsBox.get(tempId);
      if (tempRoom == null) {
        debugPrint('⚠️ Room temporaire $tempId non trouvée dans Hive');
        return;
      }
      
      // Récupérer l'ID réel depuis la réponse
      final realId = responseData['id']?.toString();
      if (realId == null || realId.isEmpty) {
        debugPrint('⚠️ ID réel non trouvé dans la réponse pour $tempId');
        return;
      }
      
      // Créer la room avec l'ID réel et marquer comme synchronisée
      final syncedRoomData = Map<String, dynamic>.from(responseData);
      syncedRoomData['is_synced'] = true;
      
      // Supprimer le temporaire et ajouter la room avec l'ID réel
      await roomsBox.delete(tempId);
      await roomsBox.put(realId, syncedRoomData);
      
      debugPrint('✅ Room $tempId remplacée par ID réel $realId');
      
      // Mettre à jour les messages qui référencent cette room temporaire
      await _updateMessagesWithNewRoomId(tempId, realId);
      
    } catch (e) {
      debugPrint('❌ Erreur mapping room $tempId: $e');
    }
  }
  
  // Met à jour les messages qui référencent une room temporaire
  Future<void> _updateMessagesWithNewRoomId(String tempRoomId, String realRoomId) async {
    try {
      final messagesBoxName = AppKeys.chatMessagesBoxName;
      
      if (!Hive.isBoxOpen(messagesBoxName)) {
        return;
      }
      
      final messagesBox = Hive.box(messagesBoxName);
      int updatedCount = 0;
      
      // Parcourir tous les messages pour mettre à jour le roomId
      for (final key in messagesBox.keys) {
        final message = messagesBox.get(key);
        if (message != null && message is Map) {
          final messageData = Map<String, dynamic>.from(message);
          if (messageData['roomId'] == tempRoomId || messageData['room_id'] == tempRoomId) {
            messageData['roomId'] = realRoomId;
            messageData['room_id'] = realRoomId;
            await messagesBox.put(key, messageData);
            updatedCount++;
          }
        }
      }
      
      if (updatedCount > 0) {
        debugPrint('✅ $updatedCount messages mis à jour avec le nouveau roomId $realRoomId');
      }
      
    } catch (e) {
      debugPrint('❌ Erreur lors de la mise à jour des messages: $e');
    }
  }

  // Méthode POST générique
  Future<Response> post(String path, {dynamic data, String? tempId}) async {
    // Vérifier la connectivité
    if (!await hasInternetConnection()) {
      // Mettre en file d'attente
      await _queueRequest(
        method: 'POST',
        path: path,
        data: data,
        tempId: tempId,
      );
      // Retourner une réponse vide pour éviter les erreurs
      return Response(
        requestOptions: RequestOptions(path: path),
        statusCode: 202, // Accepté mais pas traité
        data: {'queued': true, 'tempId': tempId},
      );
    }
    
    try {
      // Ajouter le tempId au body si présent
      final requestData = Map<String, dynamic>.from(data ?? {});
      if (tempId != null) {
        requestData['temp_id'] = tempId;
      }
      
      return await _dio.post(path, data: requestData);
    } on DioException catch (e) {
      // Si erreur réseau, mettre en file d'attente
      if (e.type == DioExceptionType.connectionTimeout ||
          e.type == DioExceptionType.connectionError ||
          e.type == DioExceptionType.unknown) {
        await _queueRequest(
          method: 'POST',
          path: path,
          data: data,
          tempId: tempId,
        );
        return Response(
          requestOptions: RequestOptions(path: path),
          statusCode: 202,
          data: {'queued': true, 'tempId': tempId},
        );
      }
      throw ApiException.fromDioException(e);
    } catch (e) {
      if (e is ApiException) rethrow;
      throw ApiException('Erreur inattendue lors de la requête POST', originalError: e);
    }
  }

  // Méthode GET générique
  Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
    // Vérifier la connectivité
    if (!await hasInternetConnection()) {
      // Mettre en file d'attente
      await _queueRequest(
        method: 'GET',
        path: path,
        queryParameters: queryParameters,
      );
      // Retourner une réponse vide
      return Response(
        requestOptions: RequestOptions(path: path),
        statusCode: 202,
        data: {'queued': true},
      );
    }

    try {
      return await _dio.get(path, queryParameters: queryParameters);
    } on DioException catch (e) {
      // Si erreur réseau, mettre en file d'attente
      if (e.type == DioExceptionType.connectionTimeout ||
          e.type == DioExceptionType.connectionError ||
          e.type == DioExceptionType.unknown) {
        await _queueRequest(
          method: 'GET',
          path: path,
          queryParameters: queryParameters,
        );
        return Response(
          requestOptions: RequestOptions(path: path),
          statusCode: 202,
          data: {'queued': true},
        );
      }
      throw ApiException.fromDioException(e);
    } catch (e) {
      if (e is ApiException) rethrow;
      throw ApiException('Erreur inattendue lors de la requête GET', originalError: e);
    }
  }

  // Méthode GET sans queue (pour les refresh de session)
  // Ne met JAMAIS la requête en file d'attente, échoue directement
  Future<Response> getWithoutQueue(String path, {Map<String, dynamic>? queryParameters}) async {
    try {
      return await _dio.get(path, queryParameters: queryParameters);
    } on DioException catch (e) {
      throw ApiException.fromDioException(e);
    } catch (e) {
      if (e is ApiException) rethrow;
      throw ApiException('Erreur inattendue lors de la requête GET', originalError: e);
    }
  }

  // Méthode PUT générique
  Future<Response> put(String path, {dynamic data, String? tempId}) async {
    // Vérifier la connectivité
    if (!await hasInternetConnection()) {
      // Mettre en file d'attente
      await _queueRequest(
        method: 'PUT',
        path: path,
        data: data,
        tempId: tempId,
      );
      // Retourner une réponse vide
      return Response(
        requestOptions: RequestOptions(path: path),
        statusCode: 202,
        data: {'queued': true, 'tempId': tempId},
      );
    }
    
    try {
      // Ajouter le tempId au body si présent
      final requestData = Map<String, dynamic>.from(data ?? {});
      if (tempId != null) {
        requestData['temp_id'] = tempId;
      }
      
      return await _dio.put(path, data: requestData);
    } on DioException catch (e) {
      // Si erreur réseau, mettre en file d'attente
      if (e.type == DioExceptionType.connectionTimeout ||
          e.type == DioExceptionType.connectionError ||
          e.type == DioExceptionType.unknown) {
        await _queueRequest(
          method: 'PUT',
          path: path,
          data: data,
          tempId: tempId,
        );
        return Response(
          requestOptions: RequestOptions(path: path),
          statusCode: 202,
          data: {'queued': true, 'tempId': tempId},
        );
      }
      throw ApiException.fromDioException(e);
    } catch (e) {
      if (e is ApiException) rethrow;
      throw ApiException('Erreur inattendue lors de la requête PUT', originalError: e);
    }
  }

  // Méthode DELETE générique
  Future<Response> delete(String path, {String? tempId}) async {
    // Vérifier la connectivité
    if (!await hasInternetConnection()) {
      // Mettre en file d'attente
      await _queueRequest(
        method: 'DELETE',
        path: path,
        tempId: tempId,
      );
      // Retourner une réponse vide
      return Response(
        requestOptions: RequestOptions(path: path),
        statusCode: 202,
        data: {'queued': true, 'tempId': tempId},
      );
    }
    
    try {
      return await _dio.delete(path);
    } on DioException catch (e) {
      // Si erreur réseau, mettre en file d'attente
      if (e.type == DioExceptionType.connectionTimeout ||
          e.type == DioExceptionType.connectionError ||
          e.type == DioExceptionType.unknown) {
        await _queueRequest(
          method: 'DELETE',
          path: path,
          tempId: tempId,
        );
        return Response(
          requestOptions: RequestOptions(path: path),
          statusCode: 202,
          data: {'queued': true, 'tempId': tempId},
        );
      }
      throw ApiException.fromDioException(e);
    } catch (e) {
      if (e is ApiException) rethrow;
      throw ApiException('Erreur inattendue lors de la requête DELETE', originalError: e);
    }
  }

  // === GESTION DES CONFLITS ===
  
  // Récupère les requêtes en conflit
  List<PendingRequest> getConflictedRequests() {
    if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
      return [];
    }
    
    final box = Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName);
    return box.values
        .where((request) => request.metadata != null && request.metadata!['hasConflict'] == true)
        .toList();
  }
  
  // Compte les requêtes en conflit
  int getConflictedRequestsCount() {
    return getConflictedRequests().length;
  }
  
  // Résout un conflit en supprimant la requête
  Future<void> resolveConflictByDeletion(String requestId) async {
    if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
      await Hive.openBox<PendingRequest>(AppKeys.pendingRequestsBoxName);
    }
    
    final box = Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName);
    final request = box.values.firstWhere(
      (r) => r.id == requestId,
      orElse: () => throw Exception('Requête non trouvée'),
    );
    
    await box.delete(request.key);
    debugPrint('🗑️ Conflit résolu par suppression de la requête ${requestId}');
  }
  
  // Résout un conflit en forçant le réessai
  Future<void> resolveConflictByRetry(String requestId) async {
    if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
      await Hive.openBox<PendingRequest>(AppKeys.pendingRequestsBoxName);
    }
    
    final box = Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName);
    final request = box.values.firstWhere(
      (r) => r.id == requestId,
      orElse: () => throw Exception('Requête non trouvée'),
    );
    
    // Retirer le marqueur de conflit
    final updatedMetadata = Map<String, dynamic>.from(request.metadata ?? {});
    updatedMetadata.remove('hasConflict');
    
    final updatedRequest = request.copyWith(
      retryCount: 0, // Réinitialiser le compteur
      errorMessage: null,
      metadata: updatedMetadata,
    );
    
    await box.put(request.key, updatedRequest);
    debugPrint('🔄 Conflit marqué pour réessai: ${requestId}');
    
    // Relancer le traitement si connecté
    if (_connectivityService?.isConnected ?? false) {
      processPendingRequests();
    }
  }
  
  // === EXPORT DES DONNÉES EN ATTENTE ===
  
  // Exporte toutes les requêtes en attente en JSON
  String exportPendingRequestsToJson() {
    if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
      return '[]';
    }
    
    final box = Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName);
    final requests = box.values.map((request) => {
      'id': request.id,
      'method': request.method,
      'path': request.path,
      'data': request.data,
      'queryParams': request.queryParams,
      'tempId': request.tempId,
      'metadata': request.metadata,
      'createdAt': request.createdAt.toIso8601String(),
      'retryCount': request.retryCount,
      'errorMessage': request.errorMessage,
      'hasConflict': request.metadata != null ? (request.metadata!['hasConflict'] ?? false) : false,
    }).toList();
    
    return jsonEncode({
      'exportDate': DateTime.now().toIso8601String(),
      'totalRequests': requests.length,
      'conflictedRequests': requests.where((r) => r['hasConflict'] == true).length,
      'requests': requests,
    });
  }
  
  // Importe des requêtes depuis un JSON (fusion avec l'existant)
  Future<int> importPendingRequestsFromJson(String jsonString) async {
    try {
      final data = jsonDecode(jsonString);
      if (data['requests'] == null || data['requests'] is! List) {
        throw FormatException('Format JSON invalide');
      }
      
      if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
        await Hive.openBox<PendingRequest>(AppKeys.pendingRequestsBoxName);
      }
      
      final box = Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName);
      
      // Vérifier la limite
      final currentCount = box.length;
      final importCount = (data['requests'] as List).length;
      if (currentCount + importCount > 1000) {
        throw ApiException(
          'Import impossible: dépassement de la limite de 1000 requêtes. '
          'Actuellement: $currentCount, À importer: $importCount',
        );
      }
      
      // Récupérer les IDs existants pour éviter les doublons
      final existingIds = box.values.map((r) => r.id).toSet();
      
      int imported = 0;
      for (final requestData in data['requests']) {
        final requestId = requestData['id'] as String;
        
        // Éviter les doublons
        if (existingIds.contains(requestId)) {
          debugPrint('⚠️ Requête ${requestId} déjà présente, ignorée');
          continue;
        }
        
        final request = PendingRequest(
          id: requestId,
          method: requestData['method'] as String,
          path: requestData['path'] as String,
          data: requestData['data'] as Map<String, dynamic>?,
          queryParams: requestData['queryParams'] as Map<String, dynamic>?,
          tempId: requestData['tempId'] as String?,
          metadata: Map<String, dynamic>.from(requestData['metadata'] ?? {}),
          createdAt: DateTime.parse(requestData['createdAt'] as String),
          context: requestData['context'] ?? 'api',
          retryCount: requestData['retryCount'] ?? 0,
          errorMessage: requestData['errorMessage'] as String?,
        );
        
        await box.add(request);
        imported++;
      }
      
      debugPrint('✅ Import terminé: $imported requêtes importées');
      return imported;
      
    } catch (e) {
      debugPrint('❌ Erreur lors de l\'import: $e');
      rethrow;
    }
  }
  
  // Obtient des statistiques sur les requêtes en attente
  Map<String, dynamic> getPendingRequestsStats() {
    if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
      return {
        'total': 0,
        'conflicted': 0,
        'failed': 0,
        'byMethod': {},
        'oldestRequest': null,
      };
    }
    
    final box = Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName);
    final requests = box.values.toList();
    
    if (requests.isEmpty) {
      return {
        'total': 0,
        'conflicted': 0,
        'failed': 0,
        'byMethod': {},
        'oldestRequest': null,
      };
    }
    
    // Trier par date pour trouver la plus ancienne
    requests.sort((a, b) => a.createdAt.compareTo(b.createdAt));
    
    // Compter par méthode
    final byMethod = <String, int>{};
    for (final request in requests) {
      byMethod[request.method] = (byMethod[request.method] ?? 0) + 1;
    }
    
    return {
      'total': requests.length,
      'conflicted': requests.where((r) => r.metadata != null && r.metadata!['hasConflict'] == true).length,
      'failed': requests.where((r) => r.retryCount >= 5).length,
      'byMethod': byMethod,
      'oldestRequest': requests.first.createdAt.toIso8601String(),
      'newestRequest': requests.last.createdAt.toIso8601String(),
    };
  }
  
  // Méthode pour uploader un logo d'amicale
  Future<Map<String, dynamic>> uploadLogo(int entiteId, dynamic imageFile) async {
    try {
      FormData formData;
      
      // Gestion différente selon la plateforme (Web ou Mobile)
      if (kIsWeb) {
        // Pour le web, imageFile est un XFile
        final bytes = await imageFile.readAsBytes();
        
        // Vérification de la taille (5 Mo max)
        const int maxSize = 5 * 1024 * 1024;
        if (bytes.length > maxSize) {
          throw ApiException(
            'Le fichier est trop volumineux. Taille maximale: 5 Mo', 
            statusCode: 413
          );
        }
        
        formData = FormData.fromMap({
          'logo': MultipartFile.fromBytes(
            bytes,
            filename: imageFile.name,
          ),
        });
      } else {
        // Pour mobile, imageFile est un File
        final fileLength = await imageFile.length();
        
        // Vérification de la taille (5 Mo max)
        const int maxSize = 5 * 1024 * 1024;
        if (fileLength > maxSize) {
          throw ApiException(
            'Le fichier est trop volumineux. Taille maximale: 5 Mo', 
            statusCode: 413
          );
        }
        
        formData = FormData.fromMap({
          'logo': await MultipartFile.fromFile(
            imageFile.path,
            filename: imageFile.path.split('/').last,
          ),
        });
      }

      final response = await _dio.post(
        '/entites/$entiteId/logo',
        data: formData,
        options: Options(
          headers: {
            'Content-Type': 'multipart/form-data',
          },
        ),
      );

      if (response.statusCode == 200 || response.statusCode == 201) {
        return response.data;
      } else {
        throw ApiException('Erreur lors de l\'upload du logo', 
          statusCode: response.statusCode);
      }
    } on DioException catch (e) {
      throw ApiException.fromDioException(e);
    } catch (e) {
      if (e is ApiException) rethrow;
      throw ApiException('Erreur inattendue lors de l\'upload du logo', originalError: e);
    }
  }

  // Authentification avec PHP session
  Future<Map<String, dynamic>> login(String username, String password, {required String type}) async {
    try {
      final response = await _dio.post(AppKeys.loginEndpoint, data: {
        'username': username,
        'password': password,
        'type': type,
      });

      final data = response.data as Map<String, dynamic>;
      final status = data['status'] as String?;

      // Si le statut n'est pas 'success', créer une exception avec le message de l'API
      if (status != 'success') {
        final message = data['message'] as String? ?? 'Erreur de connexion';
        throw ApiException(message);
      }

      // Si succès, configurer la session
      if (data.containsKey('session_id')) {
        final sessionId = data['session_id'];
        if (sessionId != null) {
          debugPrint('🔐 [LOGIN] Token reçu du serveur: $sessionId');
          debugPrint('🔐 [LOGIN] Longueur token: ${sessionId.toString().length}');
          setSessionId(sessionId);
          debugPrint('🔐 [LOGIN] Token stocké dans _sessionId');

          // Collecter et envoyer les informations du device après login réussi
          // Délai de 1 seconde pour laisser la session PHP se stabiliser
          debugPrint('📱 Collecte des informations device après login (délai 1s)...');
          Future.delayed(const Duration(seconds: 1), () {
            DeviceInfoService.instance.collectAndSendDeviceInfo().then((_) {
              debugPrint('✅ Informations device collectées et envoyées');
            }).catchError((error) {
              debugPrint('⚠️ Erreur lors de l\'envoi des infos device: $error');
              // Ne pas bloquer le login si l'envoi des infos device échoue
            });
          });
        }
      }

      return data;
    } on DioException catch (e) {
      throw ApiException.fromDioException(e);
    } catch (e) {
      if (e is ApiException) rethrow;
      throw ApiException('Erreur inattendue lors de la connexion', originalError: e);
    }
  }


  // Déconnexion
  Future<void> logout() async {
    try {
      if (_sessionId != null) {
        await _dio.post(AppKeys.logoutEndpoint);
        _sessionId = null;
      }
    } catch (e) {
      // Même en cas d'erreur, on réinitialise la session
      _sessionId = null;
      rethrow;
    }
  }

  // Utilisateurs
  Future<List<UserModel>> getUsers() async {
    try {
      final response = await retry(
        () => _dio.get('/users'),
        retryIf: (e) => e is SocketException || e is TimeoutException,
      );

      return (response.data as List).map((json) => UserModel.fromJson(json)).toList();
    } catch (e) {
      // Gérer les erreurs
      rethrow;
    }
  }

  Future<UserModel> getUserById(int id) async {
    try {
      final response = await _dio.get('/users/$id');
      return UserModel.fromJson(response.data);
    } catch (e) {
      rethrow;
    }
  }

  Future<UserModel> updateUser(UserModel user) async {
    try {
      final response = await _dio.put('/users/${user.id}', data: user.toJson());

      // Vérifier la structure de la réponse
      final data = response.data as Map<String, dynamic>;

      // Si l'API retourne {status: "success", message: "..."}
      if (data.containsKey('status') && data['status'] == 'success') {
        // L'API confirme le succès mais ne retourne pas l'objet user
        // On retourne l'utilisateur original qui a été envoyé
        debugPrint('✅ API updateUser success: ${data['message']}');
        return user;
      }

      // Si l'API retourne directement un UserModel (fallback)
      return UserModel.fromJson(data);
    } on DioException catch (e) {
      throw ApiException.fromDioException(e);
    } catch (e) {
      throw ApiException('Erreur inattendue lors de la mise à jour', originalError: e);
    }
  }

  // Appliquer la même logique aux autres méthodes
  Future<UserModel> createUser(UserModel user) async {
    try {
      final response = await _dio.post('/users', data: user.toJson());
      return UserModel.fromJson(response.data);
    } on DioException catch (e) {
      throw ApiException.fromDioException(e);
    } catch (e) {
      throw ApiException('Erreur inattendue lors de la création', originalError: e);
    }
  }

  Future<void> deleteUser(String id) async {
    try {
      await _dio.delete('/users/$id');
    } catch (e) {
      rethrow;
    }
  }

  // Synchronisation en batch
  Future<Map<String, dynamic>> syncData({
    List<UserModel>? users,
  }) async {
    try {
      final Map<String, dynamic> payload = {
        if (users != null) 'users': users.map((u) => u.toJson()).toList(),
      };

      final response = await _dio.post('/sync', data: payload);
      return response.data;
    } catch (e) {
      rethrow;
    }
  }

  // Export Excel d'une opération
  Future<void> downloadOperationExcel(int operationId, String fileName) async {
    try {
      debugPrint('📊 Téléchargement Excel pour opération $operationId');

      final response = await _dio.get(
        '/operations/$operationId/export/excel',
        options: Options(
          responseType: ResponseType.bytes, // Important pour les fichiers binaires
          headers: {
            'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
          },
        ),
      );

      if (response.statusCode == 200) {
        debugPrint('✅ Fichier Excel reçu (${response.data.length} bytes)');

        if (kIsWeb) {
          // Pour le web : déclencher le téléchargement via le navigateur
          _downloadFileWeb(response.data, fileName);
        } else {
          // Pour mobile : sauvegarder dans le dossier de téléchargements
          await _downloadFileMobile(response.data, fileName);
        }

        debugPrint('✅ Export Excel terminé: $fileName');
      } else {
        throw ApiException('Erreur lors du téléchargement: ${response.statusCode}');
      }
    } on DioException catch (e) {
      throw ApiException.fromDioException(e);
    } catch (e) {
      if (e is ApiException) rethrow;
      throw ApiException('Erreur inattendue lors de l\'export Excel', originalError: e);
    }
  }

  // Téléchargement pour le web
  void _downloadFileWeb(List<int> bytes, String fileName) {
    final blob = html.Blob([bytes]);
    final url = html.Url.createObjectUrlFromBlob(blob);

    html.AnchorElement(href: url)
      ..setAttribute('download', fileName)
      ..click();

    html.Url.revokeObjectUrl(url);
    debugPrint('🌐 Téléchargement web déclenché: $fileName');
  }

  // Téléchargement pour mobile
  Future<void> _downloadFileMobile(List<int> bytes, String fileName) async {
    try {
      // Pour mobile, on pourrait utiliser path_provider pour obtenir le dossier de téléchargements
      // et file_picker ou similar pour sauvegarder le fichier
      // Pour l'instant, on lance juste une exception informative
      throw const ApiException('Téléchargement mobile non implémenté. Utilisez la version web.');
    } catch (e) {
      rethrow;
    }
  }

  // Méthode de nettoyage pour les tests
  static void reset() {
    _instance?._connectivityService?.removeListener(_instance!._onConnectivityChanged);
    _instance?._connectivityService?.dispose();
    _instance = null;
  }
  
  // Dispose pour nettoyer les ressources
  void dispose() {
    _connectivityService?.removeListener(_onConnectivityChanged);
    _connectivityService?.dispose();
  }
}
