import 'dart:async'; import 'package:flutter/material.dart'; import '../../core/utils/app_logger.dart'; import '../../data/repositories/friends_repository_impl.dart'; import '../../data/services/realtime_notification_service.dart'; import '../../data/services/secure_storage.dart'; import '../../domain/entities/friend.dart'; import '../../domain/entities/friend_request.dart'; /// [FriendsProvider] est un `ChangeNotifier` qui gère la logique de gestion des amis. /// Il interagit avec le [FriendsRepositoryImpl] pour effectuer des appels API et gérer /// la liste des amis de l'utilisateur, avec une gestion avancée de la pagination, /// du statut des amis et de la gestion des erreurs. class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par page /// Constructeur de [FriendsProvider] qui nécessite l'instance d'un [FriendsRepositoryImpl]. FriendsProvider({required this.friendsRepository}); final FriendsRepositoryImpl friendsRepository; // Liste des amis List _friendsList = []; bool _isLoading = false; // Indicateur de chargement bool _hasMore = true; // Indicateur de pagination int _currentPage = 0; // Numéro de la page actuelle pour la pagination final int _friendsPerPage = 10; // Liste des demandes d'amitié envoyées List _sentRequests = []; bool _isLoadingSentRequests = false; int _currentSentRequestPage = 0; // Liste des demandes d'amitié reçues List _receivedRequests = []; bool _isLoadingReceivedRequests = false; int _currentReceivedRequestPage = 0; final int _requestsPerPage = 10; // Liste des suggestions d'amis List _friendSuggestions = []; bool _isLoadingSuggestions = false; // Service de notifications temps réel RealtimeNotificationService? _realtimeService; StreamSubscription? _friendRequestSubscription; // Getters pour accéder à l'état actuel des données bool get isLoading => _isLoading; bool get hasMore => _hasMore; List get friendsList => _friendsList; List get sentRequests => _sentRequests; List get receivedRequests => _receivedRequests; bool get isLoadingSentRequests => _isLoadingSentRequests; bool get isLoadingReceivedRequests => _isLoadingReceivedRequests; List get friendSuggestions => _friendSuggestions; bool get isLoadingSuggestions => _isLoadingSuggestions; // Pour compatibilité avec l'ancien code List get pendingRequests => _receivedRequests; bool get isLoadingRequests => _isLoadingReceivedRequests; /// Récupère la liste des amis pour un utilisateur donné avec pagination. /// /// [userId] : L'identifiant unique de l'utilisateur connecté. /// [loadMore] : Si vrai, charge plus d'amis, sinon recharge la liste depuis le début. /// /// Cette méthode gère : /// - La pagination de la liste d'amis. /// - L'exclusion de l'utilisateur lui-même. /// - Les erreurs et les logs pour une traçabilité complète. Future fetchFriends(String userId, {bool loadMore = false}) async { if (_isLoading) { AppLogger.w('Une opération de chargement est déjà en cours. Annulation de la nouvelle requête.', tag: 'FriendsProvider'); return; } _isLoading = true; notifyListeners(); AppLogger.i('Début du chargement des amis pour l\'utilisateur $userId.', tag: 'FriendsProvider'); // Réinitialisation de la pagination si ce n'est pas un chargement supplémentaire if (!loadMore) { _friendsList = []; _currentPage = 0; _hasMore = true; AppLogger.i('Réinitialisation de la pagination et de la liste des amis.', tag: 'FriendsProvider'); } try { AppLogger.d('Chargement de la page $_currentPage des amis pour l\'utilisateur $userId.', tag: 'FriendsProvider'); final newFriends = await friendsRepository.fetchFriends(userId, _currentPage, _friendsPerPage); // Gestion de l'absence de nouveaux amis if (newFriends.isEmpty) { _hasMore = false; AppLogger.i('Plus d\'amis à charger.', tag: 'FriendsProvider'); } else { // Ajout des amis à la liste, en excluant l'utilisateur connecté for (final friend in newFriends) { if (friend.friendId != userId) { _friendsList.add(friend); AppLogger.d('Ami ajouté : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}', tag: 'FriendsProvider'); } else { AppLogger.w("L'utilisateur connecté est exclu de la liste des amis : ${friend.friendId}", tag: 'FriendsProvider'); } } _currentPage++; AppLogger.d('Préparation de la page suivante : $_currentPage', tag: 'FriendsProvider'); } } catch (e, stackTrace) { AppLogger.e('Erreur lors du chargement des amis', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); } finally { _isLoading = false; AppLogger.d('Fin du chargement des amis.', tag: 'FriendsProvider'); notifyListeners(); } } /// Supprime un ami de la liste locale et de l'API. /// /// [friendId] : Identifiant unique de l'ami à supprimer. /// /// Cette méthode : /// - Loggue chaque étape. /// - Enlève l'ami de la liste locale. Future removeFriend(String friendId) async { try { AppLogger.i('Suppression de l\'ami avec l\'ID : $friendId', tag: 'FriendsProvider'); await friendsRepository.removeFriend(friendId); // Appel API pour supprimer l'ami _friendsList.removeWhere((friend) => friend.friendId == friendId); // Suppression locale AppLogger.i('Ami supprimé localement avec succès : $friendId', tag: 'FriendsProvider'); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la suppression de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); } finally { notifyListeners(); } } /// Récupère les détails d'un ami via l'API. /// /// [userId] : Identifiant de l'utilisateur connecté. /// [friendId] : Identifiant de l'ami dont on souhaite récupérer les détails. /// /// Retourne un `Future` contenant les détails de l'ami ou `null` en cas d'erreur. Future fetchFriendDetails(String userId, String friendId) async { try { AppLogger.d('Récupération des détails de l\'ami avec l\'ID : $friendId', tag: 'FriendsProvider'); final friendDetails = await friendsRepository.getFriendDetails(friendId, userId); if (friendDetails != null) { AppLogger.d('Détails de l\'ami récupérés avec succès : ${friendDetails.friendId}', tag: 'FriendsProvider'); } else { AppLogger.w('Détails de l\'ami introuvables pour l\'ID : $friendId', tag: 'FriendsProvider'); } return friendDetails; } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des détails de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); return null; } } /// Convertit un statut sous forme de chaîne en [FriendStatus]. /// /// [status] : Le statut sous forme de chaîne (par exemple, 'pending', 'accepted'). /// /// Retourne un [FriendStatus] correspondant, ou `FriendStatus.unknown` si non reconnu. FriendStatus _convertToFriendStatus(String status) { switch (status.toLowerCase()) { case 'pending': return FriendStatus.pending; case 'accepted': return FriendStatus.accepted; case 'blocked': return FriendStatus.blocked; default: return FriendStatus.unknown; } } /// Met à jour le statut d'un ami (ex. accepter, bloquer). /// /// [friendId] : Identifiant de l'ami dont on souhaite mettre à jour le statut. /// [status] : Nouveau statut sous forme de chaîne de caractères. /// /// Loggue l'action, met à jour le statut en local et appelle l'API pour mettre à jour le statut. Future updateFriendStatus(String friendId, String status) async { try { AppLogger.i('Mise à jour du statut de l\'ami avec l\'ID : $friendId', tag: 'FriendsProvider'); // Conversion du statut sous forme de chaîne en statut spécifique final friendStatus = _convertToFriendStatus(status); await friendsRepository.updateFriendStatus(friendId, status); // Mise à jour dans l'API // Mise à jour locale de la liste des amis avec le nouveau statut final friendIndex = _friendsList.indexWhere((friend) => friend.friendId == friendId); if (friendIndex != -1) { _friendsList[friendIndex] = _friendsList[friendIndex].copyWith(status: friendStatus); AppLogger.i('Statut de l\'ami mis à jour localement pour l\'ID : $friendId', tag: 'FriendsProvider'); } } catch (e, stackTrace) { AppLogger.e('Erreur lors de la mise à jour du statut de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); } finally { notifyListeners(); } } /// Ajoute un nouvel ami. /// /// [friendId] : L'identifiant unique de l'ami à ajouter. /// /// Cette méthode : /// - Loggue chaque étape. /// - Envoie la demande d'ami via l'API. /// - Rafraîchit la liste des amis si l'ajout réussit. Future addFriend(String friendId) async { try { // Récupérer le userId de l'utilisateur actuel final currentUserId = await _getCurrentUserId(); if (currentUserId == null || currentUserId.isEmpty) { throw Exception('Utilisateur non connecté'); } // VALIDATION: Empêcher l'utilisateur de s'ajouter lui-même comme ami if (currentUserId == friendId) { AppLogger.w('Tentative d\'ajout de soi-même comme ami bloquée', tag: 'FriendsProvider'); throw Exception('Vous ne pouvez pas vous ajouter vous-même comme ami'); } AppLogger.i('Ajout de l\'ami: userId=$currentUserId, friendId=$friendId', tag: 'FriendsProvider'); await friendsRepository.addFriend(currentUserId, friendId); AppLogger.i('Demande d\'ami envoyée avec succès', tag: 'FriendsProvider'); // Rafraîchir la liste des amis après l'ajout // Note: L'ami ne sera visible qu'après acceptation de la demande } catch (e, stackTrace) { AppLogger.e('Erreur lors de l\'ajout de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); rethrow; // Propager l'erreur pour que l'UI puisse l'afficher } finally { notifyListeners(); } } /// Récupère l'ID de l'utilisateur actuel depuis le stockage sécurisé Future _getCurrentUserId() async { try { final secureStorage = SecureStorage(); return await secureStorage.getUserId(); } catch (e) { AppLogger.e('Erreur lors de la récupération de l\'userId', error: e, tag: 'FriendsProvider'); return null; } } /// Récupère les demandes d'amitié en attente pour l'utilisateur actuel (compatibilité). Future fetchPendingRequests({bool loadMore = false}) async { await fetchReceivedRequests(loadMore: loadMore); } /// Récupère les demandes d'amitié envoyées par l'utilisateur actuel. Future fetchSentRequests({bool loadMore = false}) async { try { final currentUserId = await _getCurrentUserId(); if (currentUserId == null || currentUserId.isEmpty) { throw Exception('Utilisateur non connecté'); } if (!loadMore) { _currentSentRequestPage = 0; _sentRequests = []; } _isLoadingSentRequests = true; notifyListeners(); final page = loadMore ? _currentSentRequestPage + 1 : 0; final requests = await friendsRepository.getSentFriendRequests( currentUserId, page, _requestsPerPage, ); // VALIDATION: Pour les demandes envoyées, currentUserId doit être l'expéditeur (userId) for (final request in requests) { if (request.userId != currentUserId) { AppLogger.e( 'INCOHÉRENCE DÉTECTÉE dans fetchSentRequests: ' 'currentUserId=$currentUserId mais request.userId=${request.userId}, ' 'request.friendId=${request.friendId}, ' 'userFullName=${request.userFullName}, ' 'friendFullName=${request.friendFullName}', tag: 'FriendsProvider' ); } } if (loadMore) { _sentRequests.addAll(requests); _currentSentRequestPage = page; } else { _sentRequests = requests; _currentSentRequestPage = 0; } AppLogger.i('${requests.length} demandes d\'amitié envoyées récupérées', tag: 'FriendsProvider'); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des demandes envoyées', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); rethrow; } finally { _isLoadingSentRequests = false; notifyListeners(); } } /// Récupère les demandes d'amitié reçues par l'utilisateur actuel. Future fetchReceivedRequests({bool loadMore = false}) async { try { final currentUserId = await _getCurrentUserId(); if (currentUserId == null || currentUserId.isEmpty) { throw Exception('Utilisateur non connecté'); } if (!loadMore) { _currentReceivedRequestPage = 0; _receivedRequests = []; } _isLoadingReceivedRequests = true; notifyListeners(); final page = loadMore ? _currentReceivedRequestPage + 1 : 0; final requests = await friendsRepository.getReceivedFriendRequests( currentUserId, page, _requestsPerPage, ); // VALIDATION: Pour les demandes reçues, currentUserId doit être le destinataire (friendId) for (final request in requests) { if (request.friendId != currentUserId) { AppLogger.e( 'INCOHÉRENCE DÉTECTÉE dans fetchReceivedRequests: ' 'currentUserId=$currentUserId mais request.friendId=${request.friendId}, ' 'request.userId=${request.userId}, ' 'userFullName=${request.userFullName}, ' 'friendFullName=${request.friendFullName}', tag: 'FriendsProvider' ); } } if (loadMore) { _receivedRequests.addAll(requests); _currentReceivedRequestPage = page; } else { _receivedRequests = requests; _currentReceivedRequestPage = 0; } AppLogger.i('${requests.length} demandes d\'amitié reçues récupérées', tag: 'FriendsProvider'); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des demandes reçues', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); rethrow; } finally { _isLoadingReceivedRequests = false; notifyListeners(); } } /// Accepte une demande d'amitié. Future acceptFriendRequest(String friendshipId) async { try { AppLogger.i('Acceptation de la demande d\'amitié: $friendshipId', tag: 'FriendsProvider'); await friendsRepository.acceptFriendRequest(friendshipId); // Retirer la demande de la liste des demandes reçues _receivedRequests.removeWhere((req) => req.friendshipId == friendshipId); // Rafraîchir la liste des amis final currentUserId = await _getCurrentUserId(); if (currentUserId != null) { await fetchFriends(currentUserId); } AppLogger.i('Demande d\'amitié acceptée avec succès', tag: 'FriendsProvider'); } catch (e, stackTrace) { AppLogger.e('Erreur lors de l\'acceptation de la demande', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); rethrow; } finally { notifyListeners(); } } /// Rejette une demande d'amitié. Future rejectFriendRequest(String friendshipId) async { try { AppLogger.i('Rejet de la demande d\'amitié: $friendshipId', tag: 'FriendsProvider'); await friendsRepository.rejectFriendRequest(friendshipId); // Retirer la demande de la liste des demandes reçues _receivedRequests.removeWhere((req) => req.friendshipId == friendshipId); AppLogger.i('Demande d\'amitié rejetée avec succès', tag: 'FriendsProvider'); } catch (e, stackTrace) { AppLogger.e('Erreur lors du rejet de la demande', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); rethrow; } finally { notifyListeners(); } } /// Annule une demande d'amitié envoyée. Future cancelFriendRequest(String friendshipId) async { try { AppLogger.i('Annulation de la demande d\'amitié: $friendshipId', tag: 'FriendsProvider'); await friendsRepository.cancelFriendRequest(friendshipId); // Retirer la demande de la liste des demandes envoyées _sentRequests.removeWhere((req) => req.friendshipId == friendshipId); AppLogger.i('Demande d\'amitié annulée avec succès', tag: 'FriendsProvider'); } catch (e, stackTrace) { AppLogger.e('Erreur lors de l\'annulation de la demande', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); rethrow; } finally { notifyListeners(); } } /// Récupère les suggestions d'amis pour l'utilisateur actuel. /// /// Les suggestions sont basées sur les amis en commun et d'autres critères /// de pertinence définis par le backend. /// /// [limit] : Nombre maximum de suggestions à récupérer (par défaut 10). Future fetchFriendSuggestions({int limit = 10}) async { try { final currentUserId = await _getCurrentUserId(); if (currentUserId == null || currentUserId.isEmpty) { throw Exception('Utilisateur non connecté'); } _isLoadingSuggestions = true; notifyListeners(); AppLogger.i('Récupération des suggestions d\'amis (limit: $limit)', tag: 'FriendsProvider'); _friendSuggestions = await friendsRepository.getFriendSuggestions(currentUserId, limit: limit); AppLogger.i('${_friendSuggestions.length} suggestions d\'amis récupérées', tag: 'FriendsProvider'); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des suggestions', error: e, stackTrace: stackTrace, tag: 'FriendsProvider'); _friendSuggestions = []; rethrow; } finally { _isLoadingSuggestions = false; notifyListeners(); } } /// Connecte le service de notifications temps réel. /// /// Cette méthode doit être appelée après la connexion de l'utilisateur pour /// recevoir les notifications de demandes d'amitié en temps réel. /// /// [service] : Le service de notifications temps réel à connecter. void connectRealtime(RealtimeNotificationService service) { _realtimeService = service; // Écouter les demandes d'amitié en temps réel _friendRequestSubscription = service.friendRequestStream.listen( _handleFriendRequestNotification, onError: (error) { AppLogger.e('Erreur dans le stream de demandes d\'amitié', error: error, tag: 'FriendsProvider'); }, ); AppLogger.i('Service de notifications temps réel connecté pour les demandes d\'amitié', tag: 'FriendsProvider'); } /// Gère les notifications de demandes d'amitié reçues en temps réel. /// /// Cette méthode est appelée automatiquement lorsqu'une notification /// est reçue via WebSocket. void _handleFriendRequestNotification(FriendRequestNotification notification) { AppLogger.i('Notification de demande d\'amitié reçue: ${notification.type}', tag: 'FriendsProvider'); switch (notification.type) { case 'received': // Rafraîchir les demandes reçues pour inclure la nouvelle demande _refreshReceivedRequests(); AppLogger.i('Nouvelle demande d\'amitié de ${notification.senderName}', tag: 'FriendsProvider'); break; case 'accepted': // Rafraîchir la liste d'amis pour inclure le nouvel ami _refreshFriendsList(); // Supprimer de la liste des demandes envoyées si présente _sentRequests.removeWhere((request) => request.friendshipId == notification.requestId); notifyListeners(); AppLogger.i('${notification.senderName} a accepté votre demande', tag: 'FriendsProvider'); break; case 'rejected': // Supprimer de la liste des demandes envoyées _sentRequests.removeWhere((request) => request.friendshipId == notification.requestId); notifyListeners(); AppLogger.i('Demande d\'amitié rejetée: ${notification.requestId}', tag: 'FriendsProvider'); break; default: AppLogger.w('Type de notification inconnu: ${notification.type}', tag: 'FriendsProvider'); } } /// Rafraîchit la liste des demandes reçues en arrière-plan. Future _refreshReceivedRequests() async { try { await fetchReceivedRequests(loadMore: false); } catch (e) { AppLogger.e('Erreur lors du rafraîchissement des demandes reçues', error: e, tag: 'FriendsProvider'); } } /// Rafraîchit la liste d'amis en arrière-plan. Future _refreshFriendsList() async { try { final currentUserId = await _getCurrentUserId(); if (currentUserId == null || currentUserId.isEmpty) return; await fetchFriends(currentUserId, loadMore: false); } catch (e) { AppLogger.e('Erreur lors du rafraîchissement de la liste d\'amis', error: e, tag: 'FriendsProvider'); } } /// Déconnecte le service de notifications temps réel. void disconnectRealtime() { _friendRequestSubscription?.cancel(); _friendRequestSubscription = null; _realtimeService = null; AppLogger.i('Service de notifications temps réel déconnecté', tag: 'FriendsProvider'); } @override void dispose() { disconnectRealtime(); super.dispose(); } }