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/event_model.dart'; /// Source de données distante pour les événements. /// /// Cette classe gère toutes les opérations CRUD sur les événements /// via l'API backend. Elle inclut la gestion d'erreurs, les timeouts, /// et la validation des réponses. /// /// **Usage:** /// ```dart /// final dataSource = EventRemoteDataSource(http.Client()); /// final events = await dataSource.getAllEvents(); /// ``` class EventRemoteDataSource { /// Crée une nouvelle instance de [EventRemoteDataSource]. /// /// [client] Le client HTTP à utiliser pour les requêtes EventRemoteDataSource(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. /// /// [method] La méthode HTTP (GET, POST, PUT, DELETE, PATCH) /// [uri] L'URI de la requête /// [headers] Les headers de la requête /// [body] Le corps de la requête (optionnel) /// /// Returns la réponse HTTP /// /// Throws [ServerException] en cas d'erreur 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 '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; 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. /// /// [response] La réponse HTTP /// [expectedStatusCodes] Les codes de statut attendus (par défaut: [200]) /// /// Returns les données JSON décodées /// /// Throws [ServerException] si le code de statut n'est pas attendu 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. /// /// [response] La réponse HTTP avec erreur /// /// Throws [ServerException] avec un message approprié void _handleErrorResponse(http.Response response) { String errorMessage; try { final errorBody = json.decode(response.body); errorMessage = errorBody['message'] as String? ?? errorBody['error'] as String? ?? 'Erreur serveur inconnue'; } catch (e) { errorMessage = response.body.isNotEmpty ? response.body : 'Erreur serveur (${response.statusCode})'; } switch (response.statusCode) { case 400: throw ValidationException(errorMessage); case 401: throw UnauthorizedException(errorMessage); case 404: throw ServerException( 'Ressource non trouvée', 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, ); } } /// Log un message si le mode debug est activé. /// /// [message] Le message à logger void _log(String message) { AppLogger.d(message, tag: 'EventRemoteDataSource'); } // ============================================================================ // MÉTHODES PUBLIQUES - CRUD ÉVÉNEMENTS // ============================================================================ /// Récupère tous les événements depuis l'API. /// /// Returns une liste de [EventModel] /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// final events = await dataSource.getAllEvents(); /// ``` Future> getAllEvents() async { _log('Récupération de tous les événements'); try { final uri = Uri.parse(Urls.getAllEvents); final response = await _performRequest('GET', uri); final jsonResponse = _parseJsonResponse(response, [200]) as List?; if (jsonResponse == null) { return []; } final events = jsonResponse .map((json) => EventModel.fromJson(json as Map)) .toList(); _log('${events.length} événements récupérés avec succès'); return events; } catch (e) { _log('Erreur lors de la récupération des événements: $e'); rethrow; } } /// Récupère les événements créés par un utilisateur et ses amis. /// /// [userId] L'identifiant de l'utilisateur /// /// Returns une liste de [EventModel] /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// final events = await dataSource.getEventsCreatedByUserAndFriends('user123'); /// ``` Future> getEventsCreatedByUserAndFriends( String userId, ) async { _log('Récupération des événements pour l\'utilisateur $userId et ses amis'); if (userId.isEmpty) { throw ValidationException('L\'ID utilisateur ne peut pas être vide'); } try { final uri = Uri.parse(Urls.getEventsCreatedByUserAndFriends); final body = jsonEncode({'userId': userId}); final response = await _performRequest( 'POST', uri, body: body, ); // Gérer le cas 404 (aucun ami trouvé) comme une liste vide if (response.statusCode == 404) { _log('Aucun événement trouvé (404) - retour d\'une liste vide'); return []; } final jsonResponse = _parseJsonResponse(response, [200]) as List?; if (jsonResponse == null) { return []; } final events = jsonResponse .map((json) => EventModel.fromJson(json as Map)) .toList(); _log('${events.length} événements récupérés avec succès'); return events; } catch (e) { _log('Erreur lors de la récupération des événements: $e'); rethrow; } } /// Récupère les événements de l'utilisateur et de ses amis (avec pagination). /// /// [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 [EventModel] /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// final events = await dataSource.getEventsByFriends( /// userId: 'user123', /// page: 0, /// size: 20, /// ); /// ``` Future> getEventsByFriends({ required String userId, int page = 0, int size = 20, }) async { _log('Récupération des événements des amis pour $userId (page: $page, size: $size)'); if (userId.isEmpty) { throw ValidationException('L\'ID utilisateur ne peut pas être vide'); } try { final uri = Uri.parse( '${Urls.getEventsByFriends(userId)}?page=$page&size=$size', ); final response = await _performRequest('GET', uri); final jsonResponse = _parseJsonResponse(response, [200]) as List?; if (jsonResponse == null) { return []; } final events = jsonResponse .map((json) => EventModel.fromJson(json as Map)) .toList(); _log('${events.length} événements des amis récupérés avec succès'); return events; } catch (e) { _log('Erreur lors de la récupération des événements des amis: $e'); rethrow; } } /// Récupère un événement par son ID. /// /// [id] L'identifiant de l'événement /// /// Returns un [EventModel] /// /// Throws [ServerException] si l'événement n'est pas trouvé /// /// **Exemple:** /// ```dart /// final event = await dataSource.getEventById('event123'); /// ``` Future getEventById(String id) async { _log('Récupération de l\'événement $id'); if (id.isEmpty) { throw ValidationException('L\'ID de l\'événement ne peut pas être vide'); } try { final uri = Uri.parse(Urls.getEventByIdWithId(id)); final response = await _performRequest('GET', uri); final jsonResponse = _parseJsonResponse(response, [200]) as Map; final event = EventModel.fromJson(jsonResponse); _log('Événement $id récupéré avec succès'); return event; } catch (e) { _log('Erreur lors de la récupération de l\'événement $id: $e'); rethrow; } } /// Crée un nouvel événement. /// /// [event] Le modèle d'événement à créer /// /// Returns le [EventModel] créé avec l'ID généré par le serveur /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// final newEvent = EventModel(...); /// final createdEvent = await dataSource.createEvent(newEvent); /// ``` Future createEvent(EventModel event) async { _log('Création d\'un nouvel événement: ${event.title}'); try { final uri = Uri.parse(Urls.createEvent); final body = jsonEncode(event.toJson()); final response = await _performRequest( 'POST', uri, body: body, ); final jsonResponse = _parseJsonResponse(response, [201, 200]) as Map; final createdEvent = EventModel.fromJson(jsonResponse); _log('Événement créé avec succès: ${createdEvent.id}'); return createdEvent; } catch (e) { _log('Erreur lors de la création de l\'événement: $e'); rethrow; } } /// Met à jour un événement existant. /// /// [id] L'identifiant de l'événement à mettre à jour /// [event] Le modèle d'événement avec les nouvelles données /// /// Returns le [EventModel] mis à jour /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// final updatedEvent = event.copyWith(title: 'Nouveau titre'); /// final result = await dataSource.updateEvent('event123', updatedEvent); /// ``` Future updateEvent(String id, EventModel event) async { _log('Mise à jour de l\'événement $id'); if (id.isEmpty) { throw ValidationException('L\'ID de l\'événement ne peut pas être vide'); } try { final uri = Uri.parse(Urls.updateEventWithId(id)); final body = jsonEncode(event.toJson()); final response = await _performRequest( 'PUT', uri, body: body, ); final jsonResponse = _parseJsonResponse(response, [200]) as Map; final updatedEvent = EventModel.fromJson(jsonResponse); _log('Événement $id mis à jour avec succès'); return updatedEvent; } catch (e) { _log('Erreur lors de la mise à jour de l\'événement $id: $e'); rethrow; } } /// Supprime un événement. /// /// [id] L'identifiant de l'événement à supprimer /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// await dataSource.deleteEvent('event123'); /// ``` Future deleteEvent(String id) async { _log('Suppression de l\'événement $id'); if (id.isEmpty) { throw ValidationException('L\'ID de l\'événement ne peut pas être vide'); } try { final uri = Uri.parse(Urls.deleteEventWithId(id)); final response = await _performRequest('DELETE', uri); if (![200, 204].contains(response.statusCode)) { _handleErrorResponse(response); } _log('Événement $id supprimé avec succès'); } catch (e) { _log('Erreur lors de la suppression de l\'événement $id: $e'); rethrow; } } // ============================================================================ // MÉTHODES PUBLIQUES - ACTIONS SUR ÉVÉNEMENTS // ============================================================================ /// Participe à un événement (utilise l'endpoint participants du backend). /// /// [eventId] L'identifiant de l'événement /// [userId] L'identifiant de l'utilisateur /// /// Returns le [EventModel] mis à jour avec le nouveau participant /// /// Throws [ServerException] en cas d'erreur Future participateInEvent(String eventId, String userId) async { _log('Participation de l\'utilisateur $userId à l\'événement $eventId'); if (eventId.isEmpty || userId.isEmpty) { throw ValidationException('Les IDs ne peuvent pas être vides'); } try { // Utiliser l'endpoint participants du backend // Le backend attend un objet Users avec l'id final uri = Uri.parse(Urls.participateInEventWithId(eventId)); final body = jsonEncode({ 'id': userId, // Le backend peut aussi accepter juste l'id selon l'implémentation }); final response = await _performRequest( 'POST', uri, body: body, ); final jsonResponse = _parseJsonResponse(response, [200]) as Map; final updatedEvent = EventModel.fromJson(jsonResponse); _log('Participation réussie'); return updatedEvent; } catch (e) { _log('Erreur lors de la participation: $e'); rethrow; } } /// Réagit à un événement (utilise l'endpoint favorite du backend). /// /// [eventId] L'identifiant de l'événement /// [userId] L'identifiant de l'utilisateur /// /// Throws [ServerException] en cas d'erreur Future reactToEvent(String eventId, String userId) async { _log('Réaction de l\'utilisateur $userId à l\'événement $eventId'); if (eventId.isEmpty || userId.isEmpty) { throw ValidationException('Les IDs ne peuvent pas être vides'); } try { // Utiliser l'endpoint favorite du backend comme réaction final uri = Uri.parse(Urls.reactToEventWithId(eventId, userId)); final response = await _performRequest('POST', uri); if (![200, 201].contains(response.statusCode)) { _handleErrorResponse(response); } _log('Réaction enregistrée avec succès'); } catch (e) { _log('Erreur lors de la réaction: $e'); rethrow; } } /// Ferme un événement. /// /// [eventId] L'identifiant de l'événement à fermer /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// await dataSource.closeEvent('event123'); /// ``` Future closeEvent(String eventId) async { _log('Fermeture de l\'événement $eventId'); if (eventId.isEmpty) { throw ValidationException('L\'ID de l\'événement ne peut pas être vide'); } try { final uri = Uri.parse(Urls.closeEventWithId(eventId)); final response = await _performRequest('PATCH', uri); if (response.statusCode != 200) { _handleErrorResponse(response); } _log('Événement $eventId fermé avec succès'); } catch (e) { _log('Erreur lors de la fermeture de l\'événement $eventId: $e'); rethrow; } } /// Rouvre un événement. /// /// [eventId] L'identifiant de l'événement à rouvrir /// /// Throws [ServerException] en cas d'erreur /// /// **Exemple:** /// ```dart /// await dataSource.reopenEvent('event123'); /// ``` Future reopenEvent(String eventId) async { _log('Réouverture de l\'événement $eventId'); if (eventId.isEmpty) { throw ValidationException('L\'ID de l\'événement ne peut pas être vide'); } try { final uri = Uri.parse(Urls.reopenEventWithId(eventId)); final response = await _performRequest('PATCH', uri); if (response.statusCode != 200) { _handleErrorResponse(response); } _log('Événement $eventId rouvert avec succès'); } catch (e) { _log('Erreur lors de la réouverture de l\'événement $eventId: $e'); rethrow; } } /// Recherche des événements par mot-clé. /// /// [keyword] Le mot-clé à rechercher dans le titre et la description /// /// Returns une liste de [EventModel] correspondant à la recherche /// /// Throws [ServerException] en cas d'erreur Future> searchEvents(String keyword) async { _log('Recherche d\'événements avec le mot-clé: $keyword'); if (keyword.trim().isEmpty) { return []; } try { final uri = Uri.parse('${Urls.searchEvents}?keyword=${Uri.encodeComponent(keyword)}'); final response = await _performRequest('GET', uri); final jsonResponse = _parseJsonResponse(response, [200]) as List?; if (jsonResponse == null) { return []; } final events = jsonResponse .map((json) => EventModel.fromJson(json as Map)) .toList(); _log('${events.length} événements trouvés pour "$keyword"'); return events; } catch (e) { _log('Erreur lors de la recherche: $e'); rethrow; } } }