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 '../models/comment_model.dart'; import '../models/social_post_model.dart'; /// Source de données distante pour les posts sociaux. /// /// Cette classe gère toutes les opérations liées aux posts sociaux /// via l'API backend. Elle inclut la gestion d'erreurs, les timeouts, /// et la validation des réponses. /// /// **Usage:** /// ```dart /// final dataSource = SocialRemoteDataSource(http.Client()); /// final posts = await dataSource.getPosts(); /// ``` class SocialRemoteDataSource { /// Crée une nouvelle instance de [SocialRemoteDataSource]. /// /// [client] Le client HTTP à utiliser pour les requêtes SocialRemoteDataSource(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 { AppLogger.http(method, uri.toString()); try { http.Response response; final requestHeaders = {..._defaultHeaders, ...?headers}; switch (method) { 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 'PUT': response = await client .put(uri, headers: requestHeaders, body: body) .timeout(_timeout); break; case 'DELETE': response = await client .delete(uri, headers: requestHeaders) .timeout(_timeout); break; default: throw ArgumentError('Méthode HTTP non supportée: $method'); } AppLogger.http(method, uri.toString(), statusCode: response.statusCode); AppLogger.d('Réponse: ${response.body}', tag: 'SocialRemoteDataSource'); return response; } on SocketException catch (e, stackTrace) { AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); throw const ServerException( 'Erreur de connexion réseau. Vérifiez votre connexion internet.', ); } on http.ClientException catch (e, stackTrace) { AppLogger.e('Erreur client HTTP', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); throw ServerException('Erreur client HTTP: ${e.message}'); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la requête', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } /// Parse la réponse JSON et gère les codes de statut HTTP. dynamic _parseJsonResponse(http.Response response, List expectedStatusCodes) { if (expectedStatusCodes.contains(response.statusCode)) { if (response.body.isEmpty) { return {}; } return json.decode(response.body); } else { final errorMessage = (json.decode(response.body) as Map?)?['message'] as String? ?? 'Erreur serveur inconnue'; AppLogger.e('Erreur (${response.statusCode}): $errorMessage', tag: 'SocialRemoteDataSource'); switch (response.statusCode) { case 401: throw UnauthorizedException(errorMessage); case 404: throw ServerException('Post non trouvé: $errorMessage'); case 409: throw ConflictException(errorMessage); default: throw ServerException('Erreur serveur (${response.statusCode}): $errorMessage'); } } } // ============================================================================ // MÉTHODES PUBLIQUES // ============================================================================ /// Récupère tous les posts sociaux. /// /// [userId] L'identifiant de l'utilisateur (optionnel, pour filtrer) /// /// Returns une liste de [SocialPostModel] /// /// Throws [ServerException] en cas d'erreur Future> getPosts({String? userId}) async { AppLogger.d('Récupération des posts', tag: 'SocialRemoteDataSource'); try { final uri = userId != null ? Uri.parse(Urls.getSocialPostsByUserId(userId)) : Uri.parse(Urls.getAllPosts); final response = await _performRequest('GET', uri); final jsonList = _parseJsonResponse(response, [200]) as List; return jsonList.map((json) => SocialPostModel.fromJson(json as Map)).toList(); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des posts', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } /// Récupère les posts de l'utilisateur et de ses amis. /// /// [userId] L'identifiant de l'utilisateur /// [page] Le numéro de la page (0-indexé) /// [size] La taille de la page /// /// Returns une liste de [SocialPostModel] /// /// Throws [ServerException] en cas d'erreur Future> getPostsByFriends({ required String userId, int page = 0, int size = 20, }) async { AppLogger.d('Récupération des posts des amis pour $userId', tag: 'SocialRemoteDataSource'); try { final uri = Uri.parse( '${Urls.getSocialPostsByFriends(userId)}?page=$page&size=$size', ); final response = await _performRequest('GET', uri); final jsonList = _parseJsonResponse(response, [200]) as List; return jsonList.map((json) => SocialPostModel.fromJson(json as Map)).toList(); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des posts des amis', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } /// Crée un nouveau post social. /// /// [content] Le contenu du post /// [userId] L'identifiant de l'utilisateur créateur /// [imageUrl] URL de l'image (optionnel) /// /// Returns le [SocialPostModel] créé /// /// Throws [ServerException] en cas d'erreur Future createPost({ required String content, required String userId, String? imageUrl, }) async { AppLogger.i('Création de post pour $userId', tag: 'SocialRemoteDataSource'); try { final uri = Uri.parse(Urls.createSocialPost); final body = jsonEncode({ 'content': content, 'userId': userId, if (imageUrl != null) 'imageUrl': imageUrl, }); final response = await _performRequest('POST', uri, body: body); final jsonResponse = _parseJsonResponse(response, [201]) as Map; return SocialPostModel.fromJson(jsonResponse); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la création du post', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } /// Recherche des posts sociaux. /// /// [query] Le terme de recherche /// /// Returns une liste de [SocialPostModel] correspondant à la recherche /// /// Throws [ServerException] en cas d'erreur Future> searchPosts(String query) async { AppLogger.d('Recherche: $query', tag: 'SocialRemoteDataSource'); try { final uri = Uri.parse(Urls.searchSocialPostsWithQuery(query)); final response = await _performRequest('GET', uri); final jsonList = _parseJsonResponse(response, [200]) as List; return jsonList.map((json) => SocialPostModel.fromJson(json as Map)).toList(); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la recherche', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } /// Like un post. /// /// [postId] L'ID du post /// /// Returns le [SocialPostModel] mis à jour /// /// Throws [ServerException] en cas d'erreur Future likePost(String postId) async { AppLogger.d('Like du post: $postId', tag: 'SocialRemoteDataSource'); try { final uri = Uri.parse(Urls.likeSocialPostWithId(postId)); final response = await _performRequest('POST', uri); final jsonResponse = _parseJsonResponse(response, [200]) as Map; return SocialPostModel.fromJson(jsonResponse); } catch (e, stackTrace) { AppLogger.e('Erreur lors du like', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } /// Ajoute un commentaire à un post. /// /// [postId] L'ID du post /// /// Returns le [SocialPostModel] mis à jour /// /// Throws [ServerException] en cas d'erreur Future commentPost(String postId) async { AppLogger.d('Commentaire sur le post: $postId', tag: 'SocialRemoteDataSource'); try { final uri = Uri.parse(Urls.commentSocialPostWithId(postId)); final response = await _performRequest('POST', uri); final jsonResponse = _parseJsonResponse(response, [200]) as Map; return SocialPostModel.fromJson(jsonResponse); } catch (e, stackTrace) { AppLogger.e('Erreur lors du commentaire', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } /// Partage un post. /// /// [postId] L'ID du post /// /// Returns le [SocialPostModel] mis à jour /// /// Throws [ServerException] en cas d'erreur Future sharePost(String postId) async { AppLogger.d('Partage du post: $postId', tag: 'SocialRemoteDataSource'); try { final uri = Uri.parse(Urls.shareSocialPostWithId(postId)); final response = await _performRequest('POST', uri); final jsonResponse = _parseJsonResponse(response, [200]) as Map; return SocialPostModel.fromJson(jsonResponse); } catch (e, stackTrace) { AppLogger.e('Erreur lors du commentaire', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } /// Supprime un post. /// /// [postId] L'ID du post /// /// Throws [ServerException] en cas d'erreur Future deletePost(String postId) async { AppLogger.i('Suppression du post: $postId', tag: 'SocialRemoteDataSource'); try { final uri = Uri.parse(Urls.deleteSocialPostWithId(postId)); final response = await _performRequest('DELETE', uri); _parseJsonResponse(response, [200, 204]); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la suppression du post', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } // ============================================================================ // MÉTHODES POUR LES COMMENTAIRES // ============================================================================ /// Récupère tous les commentaires d'un post. /// /// [postId] L'ID du post /// /// Returns une liste de [CommentModel] /// /// Throws [ServerException] en cas d'erreur Future> getComments(String postId) async { AppLogger.d('Récupération des commentaires pour le post: $postId', tag: 'SocialRemoteDataSource'); try { final uri = Uri.parse(Urls.getCommentsForPost(postId)); final response = await _performRequest('GET', uri); final jsonList = _parseJsonResponse(response, [200]) as List; return jsonList.map((json) => CommentModel.fromJson(json as Map)).toList(); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des commentaires', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } /// Crée un nouveau commentaire sur un post. /// /// [postId] L'ID du post /// [content] Le contenu du commentaire /// [userId] L'ID de l'utilisateur créateur /// /// Returns le [CommentModel] créé /// /// Throws [ServerException] en cas d'erreur Future createComment({ required String postId, required String content, required String userId, }) async { AppLogger.i('Création de commentaire pour le post: $postId', tag: 'SocialRemoteDataSource'); try { final uri = Uri.parse(Urls.commentSocialPostWithId(postId)); final body = jsonEncode({ 'content': content, 'userId': userId, }); final response = await _performRequest('POST', uri, body: body); final jsonResponse = _parseJsonResponse(response, [200, 201]) as Map; return CommentModel.fromJson(jsonResponse); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la création du commentaire', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } /// Supprime un commentaire. /// /// [postId] L'ID du post /// [commentId] L'ID du commentaire /// /// Throws [ServerException] en cas d'erreur Future deleteComment(String postId, String commentId) async { AppLogger.i('Suppression du commentaire: $commentId', tag: 'SocialRemoteDataSource'); try { final uri = Uri.parse('${Urls.getCommentsForPost(postId)}/$commentId'); final response = await _performRequest('DELETE', uri); _parseJsonResponse(response, [200, 204]); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la suppression du commentaire', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource'); rethrow; } } }