import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; import '../../core/constants/env_config.dart'; import '../../core/constants/urls.dart'; import '../../core/errors/exceptions.dart'; import '../../core/utils/app_logger.dart'; import '../../domain/entities/friend.dart'; import '../../domain/entities/friend_request.dart'; import 'friends_repository.dart'; /// Implémentation de [FriendsRepository] pour gérer les appels API relatifs aux amis. /// /// Cette classe gère toutes les opérations sur les amis via l'API backend, /// avec gestion d'erreurs robuste, timeouts, et validation des réponses. /// /// **Usage:** /// ```dart /// final repository = FriendsRepositoryImpl(client: http.Client()); /// final friends = await repository.fetchFriends('user123', 0, 20); /// ``` class FriendsRepositoryImpl implements FriendsRepository { /// Crée une nouvelle instance de [FriendsRepositoryImpl]. /// /// [client] Le client HTTP à utiliser pour les requêtes FriendsRepositoryImpl({required this.client}); /// Client HTTP pour effectuer les requêtes réseau final http.Client client; /// Headers par défaut pour les requêtes static const Map _defaultHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json', }; /// Timeout pour les requêtes réseau Duration get _timeout => Duration(seconds: EnvConfig.networkTimeout); // ============================================================================ // MÉTHODES PRIVÉES UTILITAIRES // ============================================================================ /// Effectue une requête HTTP avec gestion d'erreurs et timeout. Future _performRequest( String method, Uri uri, { Map? headers, Object? body, }) async { try { final requestHeaders = { ..._defaultHeaders, if (headers != null) ...headers, }; http.Response response; switch (method.toUpperCase()) { case 'GET': response = await client .get(uri, headers: requestHeaders) .timeout(_timeout); break; case 'POST': response = await client .post(uri, headers: requestHeaders, body: body) .timeout(_timeout); break; case 'DELETE': response = await client .delete(uri, headers: requestHeaders) .timeout(_timeout); break; case 'PATCH': response = await client .patch(uri, headers: requestHeaders, body: body) .timeout(_timeout); break; default: throw ArgumentError('Méthode HTTP non supportée: $method'); } return response; } on SocketException { throw ServerException( 'Erreur de connexion réseau. Vérifiez votre connexion Internet.', statusCode: null, ); } on HttpException catch (e) { throw ServerException( 'Erreur HTTP: ${e.message}', statusCode: null, ); } on FormatException catch (e) { throw ServerException( 'Erreur de format de réponse: ${e.message}', statusCode: null, ); } catch (e) { if (e is ServerException) rethrow; throw ServerException( 'Erreur inattendue: $e', statusCode: null, originalError: e, ); } } /// Parse une réponse JSON et gère les erreurs. dynamic _parseJsonResponse( http.Response response, List expectedStatusCodes, ) { if (!expectedStatusCodes.contains(response.statusCode)) { _handleErrorResponse(response); } try { if (response.body.isEmpty) { return null; } return json.decode(response.body); } on FormatException catch (e) { throw ServerException( 'Erreur de parsing JSON: ${e.message}', statusCode: response.statusCode, ); } } /// Gère les erreurs de réponse HTTP. void _handleErrorResponse(http.Response response) { String errorMessage; try { if (response.body.isNotEmpty) { final errorBody = json.decode(response.body); // Gérer le format Bean Validation (Quarkus) if (errorBody is Map && errorBody.containsKey('attributeName')) { final attributeName = errorBody['attributeName'] as String? ?? 'champ'; final value = errorBody['value'] as String?; final objectName = errorBody['objectName'] as String? ?? ''; // Construire un message d'erreur plus clair if (attributeName == 'friendId' && value != null) { errorMessage = 'L\'identifiant "$value" n\'est pas valide. Veuillez utiliser l\'ID utilisateur (UUID) et non l\'email.'; } else { errorMessage = 'Erreur de validation sur le champ "$attributeName"'; if (value != null) { errorMessage += ': valeur "$value" invalide'; } } } else { // Essayer plusieurs formats de réponse d'erreur standard errorMessage = errorBody['message'] as String? ?? errorBody['error'] as String? ?? errorBody['errorMessage'] as String? ?? (errorBody is Map && errorBody.isNotEmpty ? errorBody.values.first.toString() : 'Erreur serveur inconnue'); } // Log détaillé pour le débogage if (EnvConfig.enableDetailedLogs) { _log('Réponse d\'erreur du serveur (${response.statusCode}): ${response.body}'); _log('Message d\'erreur extrait: $errorMessage'); } } else { errorMessage = 'Erreur serveur (${response.statusCode})'; } } catch (e) { // Si le parsing JSON échoue, utiliser le body brut errorMessage = response.body.isNotEmpty ? response.body : 'Erreur serveur (${response.statusCode})'; if (EnvConfig.enableDetailedLogs) { _log('Erreur lors du parsing de la réponse d\'erreur: $e'); _log('Body brut: ${response.body}'); } } switch (response.statusCode) { case 400: throw ValidationException(errorMessage); case 401: throw UnauthorizedException(errorMessage); case 404: throw ServerException( 'Ressource non trouvée: $errorMessage', statusCode: 404, ); case 409: throw ConflictException(errorMessage); case 500: case 502: case 503: throw ServerException( 'Erreur serveur: $errorMessage', statusCode: response.statusCode, ); default: throw ServerException( errorMessage, statusCode: response.statusCode, ); } } /// Vérifie si une chaîne est un UUID valide bool _isValidUUID(String value) { final uuidRegex = RegExp( r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false, ); return uuidRegex.hasMatch(value); } /// Log un message si le mode debug est activé. void _log(String message) { AppLogger.d(message, tag: 'FriendsRepositoryImpl'); } // ============================================================================ // MÉTHODES PUBLIQUES // ============================================================================ /// Récupère la liste paginée des amis pour un utilisateur donné. /// /// [userId] L'identifiant unique de l'utilisateur /// [page] Le numéro de page (commence à 0) /// [size] Le nombre d'amis par page /// /// Returns une liste d'objets [Friend] /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// final friends = await repository.fetchFriends('user123', 0, 20); /// ``` @override Future> fetchFriends(String userId, int page, int size) async { _log('Récupération des amis pour l\'utilisateur $userId (page $page, taille $size)'); if (userId.isEmpty) { throw ValidationException('L\'ID utilisateur ne peut pas être vide'); } if (page < 0) { throw ValidationException('Le numéro de page doit être >= 0'); } if (size <= 0) { throw ValidationException('La taille de page doit être > 0'); } try { final uri = Uri.parse('${Urls.friendsBase}/list/$userId') .replace(queryParameters: { 'page': page.toString(), 'size': size.toString(), }); final response = await _performRequest('GET', uri); // Gérer le cas 404 comme une liste vide if (response.statusCode == 404) { _log('Aucun ami trouvé (404) - retour d\'une liste vide'); return []; } final jsonResponse = _parseJsonResponse(response, [200]) as List?; if (jsonResponse == null) { return []; } final friends = jsonResponse .map((json) => Friend.fromJson(json as Map)) .toList(); _log('${friends.length} amis récupérés avec succès'); return friends; } catch (e) { _log('Erreur lors de la récupération des amis: $e'); rethrow; } } /// Envoie une demande pour ajouter un nouvel ami. /// /// [userId] L'identifiant unique de l'utilisateur qui envoie la demande /// [friendId] L'identifiant unique de l'ami à ajouter /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// await repository.addFriend('user-uuid', 'friend-uuid'); /// ``` @override Future addFriend(String userId, String friendId) async { _log('Ajout de l\'ami: userId=$userId, friendId=$friendId'); if (userId.isEmpty) { throw ValidationException('L\'ID de l\'utilisateur ne peut pas être vide'); } if (friendId.isEmpty) { throw ValidationException('L\'ID de l\'ami ne peut pas être vide'); } try { final uri = Uri.parse('${Urls.friendsBase}/send'); // Le backend attend userId et friendId dans FriendshipCreateOneRequestDTO final bodyJson = { 'userId': userId, 'friendId': friendId, }; final body = jsonEncode(bodyJson); // Log détaillé du body envoyé if (EnvConfig.enableDetailedLogs) { _log('Envoi de la demande d\'ami à: $uri'); _log('Body JSON: $body'); } final response = await _performRequest( 'POST', uri, body: body, ); // Log de la réponse if (EnvConfig.enableDetailedLogs) { _log('Réponse du serveur (${response.statusCode}): ${response.body}'); } if (![200, 201].contains(response.statusCode)) { _handleErrorResponse(response); } _log('Ami ajouté avec succès: $friendId'); } catch (e) { _log('Erreur lors de l\'ajout de l\'ami: $e'); rethrow; } } /// Supprime un ami existant. /// /// [friendId] L'identifiant unique de l'ami à supprimer /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// await repository.removeFriend('friend123'); /// ``` @override Future removeFriend(String friendId) async { _log('Suppression de l\'ami $friendId'); if (friendId.isEmpty) { throw ValidationException('L\'ID de l\'ami ne peut pas être vide'); } try { final uri = Uri.parse('${Urls.friendsBase}/$friendId'); final response = await _performRequest('DELETE', uri); if (![200, 204].contains(response.statusCode)) { _handleErrorResponse(response); } _log('Ami $friendId supprimé avec succès'); } catch (e) { _log('Erreur lors de la suppression de l\'ami $friendId: $e'); rethrow; } } /// Récupère les détails d'un ami. /// /// [friendId] L'identifiant unique de l'ami /// [userId] L'identifiant unique de l'utilisateur connecté /// /// Returns un [Friend] avec les informations de l'ami, ou `null` si non trouvé /// /// **Exemple:** /// ```dart /// final friend = await repository.getFriendDetails('friend123', 'user123'); /// ``` @override Future getFriendDetails(String friendId, String userId) async { _log('Récupération des détails de l\'ami $friendId pour l\'utilisateur $userId'); if (friendId.isEmpty || userId.isEmpty) { throw ValidationException('Les IDs ne peuvent pas être vides'); } try { final uri = Uri.parse('${Urls.friendsBase}/details'); final body = jsonEncode({ 'friendId': friendId, 'userId': userId, }); final response = await _performRequest( 'POST', uri, body: body, ); // Gérer le cas 404 comme null if (response.statusCode == 404) { _log('Ami $friendId non trouvé (404)'); return null; } final jsonResponse = _parseJsonResponse(response, [200]) as Map?; if (jsonResponse == null) { return null; } final friend = Friend.fromJson(jsonResponse); _log('Détails de l\'ami $friendId récupérés avec succès'); return friend; } catch (e) { _log('Erreur lors de la récupération des détails de l\'ami $friendId: $e'); rethrow; } } /// Met à jour le statut d'un ami (par exemple, "accepté", "bloqué"). /// /// [friendId] L'identifiant unique de l'ami /// [status] Le nouveau statut sous forme de chaîne de caractères /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// await repository.updateFriendStatus('friend123', 'accepted'); /// ``` @override Future updateFriendStatus(String friendId, String status) async { _log('Mise à jour du statut de l\'ami $friendId: $status'); if (friendId.isEmpty) { throw ValidationException('L\'ID de l\'ami ne peut pas être vide'); } if (status.isEmpty) { throw ValidationException('Le statut ne peut pas être vide'); } try { final uri = Uri.parse('${Urls.friendsBase}/$friendId/status'); final body = jsonEncode({'status': status}); final response = await _performRequest( 'PATCH', uri, body: body, ); if (response.statusCode != 200) { _handleErrorResponse(response); } _log('Statut de l\'ami $friendId mis à jour avec succès'); } catch (e) { _log('Erreur lors de la mise à jour du statut de l\'ami $friendId: $e'); rethrow; } } /// Récupère les demandes d'amitié en attente pour un utilisateur. /// /// [userId] L'identifiant unique de l'utilisateur /// [page] Le numéro de la page pour la pagination /// [size] La taille de la page /// /// Retourne une liste de [FriendRequest] @override Future> getPendingFriendRequests(String userId, int page, int size) async { _log('Récupération des demandes d\'amitié en attente pour l\'utilisateur $userId (page $page, taille $size)'); try { final uri = Uri.parse(Urls.getPendingFriendRequestsWithUserId(userId, page: page, size: size)); final response = await _performRequest('GET', uri); if (response.statusCode != 200) { _handleErrorResponse(response); } final jsonResponse = _parseJsonResponse(response, [200]) as List; final requests = jsonResponse .map((json) => FriendRequest.fromJson(json as Map)) .toList(); _log('${requests.length} demandes d\'amitié en attente récupérées avec succès'); return requests; } catch (e) { _log('Erreur lors de la récupération des demandes d\'amitié en attente: $e'); rethrow; } } /// Accepte une demande d'amitié. /// /// [friendshipId] L'identifiant unique de la relation d'amitié /// /// Throws [ServerException] en cas d'erreur @override Future acceptFriendRequest(String friendshipId) async { _log('Acceptation de la demande d\'amitié: $friendshipId'); if (friendshipId.isEmpty) { throw ValidationException('L\'ID de la relation d\'amitié ne peut pas être vide'); } try { final uri = Uri.parse(Urls.acceptFriendRequestWithId(friendshipId)); final response = await _performRequest('PATCH', uri); if (response.statusCode != 200) { _handleErrorResponse(response); } _log('Demande d\'amitié acceptée avec succès: $friendshipId'); } catch (e) { _log('Erreur lors de l\'acceptation de la demande d\'amitié: $e'); rethrow; } } /// Rejette une demande d'amitié. /// /// [friendshipId] L'identifiant unique de la relation d'amitié /// /// Throws [ServerException] en cas d'erreur @override Future rejectFriendRequest(String friendshipId) async { _log('Rejet de la demande d\'amitié: $friendshipId'); if (friendshipId.isEmpty) { throw ValidationException('L\'ID de la relation d\'amitié ne peut pas être vide'); } try { final uri = Uri.parse(Urls.rejectFriendRequestWithId(friendshipId)); final response = await _performRequest('PATCH', uri); if (response.statusCode != 204) { _handleErrorResponse(response); } _log('Demande d\'amitié rejetée avec succès: $friendshipId'); } catch (e) { _log('Erreur lors du rejet de la demande d\'amitié: $e'); rethrow; } } /// Récupère les demandes d'amitié envoyées par un utilisateur. /// /// [userId] L'identifiant unique de l'utilisateur /// [page] Le numéro de page (commence à 0) /// [size] Le nombre de demandes par page /// /// Returns une liste d'objets [FriendRequest] /// /// Throws [ServerException] en cas d'erreur @override Future> getSentFriendRequests(String userId, int page, int size) async { _log('Récupération des demandes d\'amitié envoyées pour l\'utilisateur $userId (page $page, taille $size)'); try { final uri = Uri.parse(Urls.getSentFriendRequestsWithUserId(userId, page: page, size: size)); final response = await _performRequest('GET', uri); if (response.statusCode == 404) { _log('Aucune demande envoyée trouvée (404) - retour d\'une liste vide'); return []; } final jsonResponse = _parseJsonResponse(response, [200]) as List; final requests = jsonResponse .map((json) => FriendRequest.fromJson(json as Map)) .toList(); _log('${requests.length} demandes d\'amitié envoyées récupérées avec succès'); return requests; } catch (e) { _log('Erreur lors de la récupération des demandes d\'amitié envoyées: $e'); rethrow; } } /// Récupère les demandes d'amitié reçues par un utilisateur. /// /// [userId] L'identifiant unique de l'utilisateur /// [page] Le numéro de page (commence à 0) /// [size] Le nombre de demandes par page /// /// Returns une liste d'objets [FriendRequest] /// /// Throws [ServerException] en cas d'erreur @override Future> getReceivedFriendRequests(String userId, int page, int size) async { _log('Récupération des demandes d\'amitié reçues pour l\'utilisateur $userId (page $page, taille $size)'); try { final uri = Uri.parse(Urls.getReceivedFriendRequestsWithUserId(userId, page: page, size: size)); final response = await _performRequest('GET', uri); if (response.statusCode == 404) { _log('Aucune demande reçue trouvée (404) - retour d\'une liste vide'); return []; } final jsonResponse = _parseJsonResponse(response, [200]) as List; if (EnvConfig.enableDetailedLogs) { _log('Réponse JSON brute: $jsonResponse'); } final requests = jsonResponse .map((json) { if (EnvConfig.enableDetailedLogs) { _log('Parsing demande: $json'); } return FriendRequest.fromJson(json as Map); }) .toList(); _log('${requests.length} demandes d\'amitié reçues récupérées avec succès'); if (EnvConfig.enableDetailedLogs && requests.isNotEmpty) { _log('Première demande: userId=${requests.first.userId}, friendId=${requests.first.friendId}, userFullName=${requests.first.userFullName}'); } return requests; } catch (e) { _log('Erreur lors de la récupération des demandes d\'amitié reçues: $e'); rethrow; } } /// Annule une demande d'amitié envoyée (supprime la relation). /// /// [friendshipId] L'identifiant unique de la relation d'amitié /// /// Throws [ServerException] en cas d'erreur @override Future cancelFriendRequest(String friendshipId) async { _log('Annulation de la demande d\'amitié: $friendshipId'); if (friendshipId.isEmpty) { throw ValidationException('L\'ID de la relation d\'amitié ne peut pas être vide'); } try { // Utiliser l'endpoint DELETE existant pour supprimer la relation final uri = Uri.parse('${Urls.friendsBase}/$friendshipId'); final response = await _performRequest('DELETE', uri); if (response.statusCode != 204) { _handleErrorResponse(response); } _log('Demande d\'amitié annulée avec succès: $friendshipId'); } catch (e) { _log('Erreur lors de l\'annulation de la demande d\'amitié: $e'); rethrow; } } /// Récupère les suggestions d'amis pour un utilisateur. /// /// [userId] L'identifiant unique de l'utilisateur /// [limit] Nombre maximum de suggestions (par défaut 10) /// /// Returns une liste d'objets [FriendSuggestion] /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// final suggestions = await repository.getFriendSuggestions('user123', limit: 5); /// ``` @override Future> getFriendSuggestions(String userId, {int limit = 10}) async { _log('Récupération des suggestions d\'amis pour l\'utilisateur $userId (limit: $limit)'); if (userId.isEmpty) { throw ValidationException('L\'ID utilisateur ne peut pas être vide'); } if (limit <= 0) { throw ValidationException('La limite doit être > 0'); } try { final uri = Uri.parse(Urls.getFriendSuggestionsWithUserId(userId, limit: limit)); final response = await _performRequest('GET', uri); if (response.statusCode == 404) { _log('Aucune suggestion trouvée (404) - retour d\'une liste vide'); return []; } final jsonResponse = _parseJsonResponse(response, [200]) as List?; if (jsonResponse == null) { return []; } final suggestions = jsonResponse .map((json) => json as Map) .toList(); _log('${suggestions.length} suggestions d\'amis récupérées avec succès'); return suggestions; } catch (e) { _log('Erreur lors de la récupération des suggestions d\'amis: $e'); rethrow; } } }