import 'dart:async'; 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/chat_message_model.dart'; import '../models/conversation_model.dart'; /// Source de données distante pour le chat. /// /// Cette classe gère toutes les opérations liées au chat /// via l'API backend (REST) et WebSocket pour le temps réel. class ChatRemoteDataSource { ChatRemoteDataSource(this.client); final http.Client client; static const Map _defaultHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json', }; Duration get _timeout => Duration(seconds: EnvConfig.networkTimeout); 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: 'ChatRemoteDataSource'); return response; } on TimeoutException catch (e, stackTrace) { AppLogger.e('Timeout lors de la requête $method $uri', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); throw ServerException('La requête a pris trop de temps. Le serveur ne répond pas.'); } on SocketException catch (e, stackTrace) { AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); 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: 'ChatRemoteDataSource'); throw ServerException('Erreur client HTTP: ${e.message}'); } catch (e, stackTrace) { AppLogger.e('Erreur inattendue', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } 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: 'ChatRemoteDataSource'); switch (response.statusCode) { case 401: throw UnauthorizedException(errorMessage); case 404: throw ServerException('Ressource non trouvée: $errorMessage'); default: throw ServerException('Erreur serveur (${response.statusCode}): $errorMessage'); } } } /// Récupère toutes les conversations d'un utilisateur. Future> getConversations(String userId) async { AppLogger.d('Récupération des conversations pour: $userId', tag: 'ChatRemoteDataSource'); try { final uri = Uri.parse(Urls.getUserConversations(userId)); final response = await _performRequest('GET', uri); final jsonList = _parseJsonResponse(response, [200]) as List; return jsonList.map((json) => ConversationModel.fromJson(json as Map)).toList(); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des conversations', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } /// Récupère ou crée une conversation avec un utilisateur. /// /// Si la conversation n'existe pas (404) ou si le backend ne répond pas (timeout), /// elle sera créée automatiquement en envoyant un message initial. Future getOrCreateConversation(String userId, String participantId) async { AppLogger.d('Récupération/création conversation: $userId <-> $participantId', tag: 'ChatRemoteDataSource'); final uri = Uri.parse(Urls.getConversationBetweenUsers(userId, participantId)); try { // Essayer de récupérer la conversation existante avec un timeout plus court final response = await _performRequestWithTimeout('GET', uri, timeout: const Duration(seconds: 10)); // Si la conversation existe, la retourner if (response.statusCode == 200 || response.statusCode == 201) { final jsonResponse = json.decode(response.body) as Map; return ConversationModel.fromJson(jsonResponse); } // Si 404, la conversation n'existe pas, on doit la créer if (response.statusCode == 404) { AppLogger.i('Conversation non trouvée (404), création en cours...', tag: 'ChatRemoteDataSource'); return await _createConversationBySendingMessage(userId, participantId, uri); } // Pour les autres codes d'erreur, utiliser la méthode standard de parsing final jsonResponse = _parseJsonResponse(response, [200, 201]) as Map; return ConversationModel.fromJson(jsonResponse); } on ServerException catch (e) { // Si c'est une ServerException avec 404 ou timeout, on essaie de créer la conversation if (e.message.contains('404') || e.message.contains('non trouvée') || e.message.contains('not found') || e.message.contains('trop de temps') || e.message.contains('ne répond pas')) { AppLogger.i('Conversation non trouvée ou timeout, création en cours...', tag: 'ChatRemoteDataSource'); return await _createConversationBySendingMessage(userId, participantId, uri); } rethrow; } on TimeoutException catch (e, stackTrace) { // Si timeout, créer directement la conversation AppLogger.w('Timeout lors de la récupération de la conversation, création directe...', tag: 'ChatRemoteDataSource'); return await _createConversationBySendingMessage(userId, participantId, uri); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération/création de conversation', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } /// Effectue une requête HTTP avec un timeout personnalisé. Future _performRequestWithTimeout( String method, Uri uri, { Map? headers, Object? body, Duration? timeout, }) async { AppLogger.http(method, uri.toString()); try { http.Response response; final requestHeaders = {..._defaultHeaders, ...?headers}; final requestTimeout = timeout ?? _timeout; switch (method) { case 'GET': response = await client.get(uri, headers: requestHeaders).timeout(requestTimeout); break; case 'POST': response = await client.post(uri, headers: requestHeaders, body: body).timeout(requestTimeout); break; case 'PUT': response = await client.put(uri, headers: requestHeaders, body: body).timeout(requestTimeout); break; case 'DELETE': response = await client.delete(uri, headers: requestHeaders).timeout(requestTimeout); 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: 'ChatRemoteDataSource'); return response; } on TimeoutException catch (e, stackTrace) { AppLogger.e('Timeout lors de la requête $method $uri', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); throw ServerException('La requête a pris trop de temps. Le serveur ne répond pas.'); } on SocketException catch (e, stackTrace) { AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); 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: 'ChatRemoteDataSource'); throw ServerException('Erreur client HTTP: ${e.message}'); } catch (e, stackTrace) { AppLogger.e('Erreur inattendue', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } /// Crée une conversation en envoyant un message initial, puis récupère la conversation créée. Future _createConversationBySendingMessage( String userId, String participantId, Uri conversationUri, ) async { try { // Créer la conversation en envoyant un message initial // Le backend créera automatiquement la conversation lors de l'envoi du premier message AppLogger.i('Création de la conversation en envoyant un message initial...', tag: 'ChatRemoteDataSource'); await sendMessage( senderId: userId, recipientId: participantId, content: '👋', // Message initial pour créer la conversation messageType: 'text', ); AppLogger.i('Message initial envoyé, attente de la création de la conversation...', tag: 'ChatRemoteDataSource'); // Attendre un peu pour que le backend crée la conversation await Future.delayed(const Duration(milliseconds: 1000)); // Récupérer la conversation nouvellement créée avec plusieurs tentatives et timeout court for (int attempt = 0; attempt < 3; attempt++) { try { AppLogger.d('Tentative ${attempt + 1}/3 pour récupérer la conversation créée...', tag: 'ChatRemoteDataSource'); final retryResponse = await _performRequestWithTimeout( 'GET', conversationUri, timeout: const Duration(seconds: 5), ); if (retryResponse.statusCode == 200 || retryResponse.statusCode == 201) { final jsonResponse = json.decode(retryResponse.body) as Map; AppLogger.i('Conversation créée avec succès', tag: 'ChatRemoteDataSource'); return ConversationModel.fromJson(jsonResponse); } } on TimeoutException { AppLogger.w('Timeout lors de la tentative ${attempt + 1}, nouvelle tentative...', tag: 'ChatRemoteDataSource'); } // Attendre un peu plus avant la prochaine tentative if (attempt < 2) { await Future.delayed(Duration(milliseconds: 500 * (attempt + 1))); } } // Si après 3 tentatives on n'a toujours pas la conversation, lever une exception throw ServerException( 'Impossible de créer ou récupérer la conversation après plusieurs tentatives. ' 'Le serveur ne répond pas correctement.', ); } catch (e, stackTrace) { AppLogger.e( 'Erreur lors de la création de la conversation via message initial', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource', ); rethrow; } } /// Récupère tous les messages d'une conversation. Future> getMessages(String conversationId, {int page = 0, int size = 50}) async { AppLogger.d('Récupération des messages: $conversationId (page: $page)', tag: 'ChatRemoteDataSource'); try { final uri = Uri.parse(Urls.getConversationMessages(conversationId, page: page, size: size)); final response = await _performRequest('GET', uri); final jsonList = _parseJsonResponse(response, [200]) as List; return jsonList.map((json) => ChatMessageModel.fromJson(json as Map)).toList(); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des messages', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } /// Envoie un nouveau message dans une conversation. Future sendMessage({ required String senderId, required String recipientId, required String content, String? messageType, String? mediaUrl, }) async { AppLogger.i('Envoi message de $senderId à $recipientId', tag: 'ChatRemoteDataSource'); try { final uri = Uri.parse(Urls.sendMessage); final body = json.encode({ 'senderId': senderId, 'recipientId': recipientId, 'content': content, 'messageType': messageType ?? 'text', if (mediaUrl != null) 'mediaUrl': mediaUrl, }); final response = await _performRequest('POST', uri, body: body); final jsonResponse = _parseJsonResponse(response, [200, 201]) as Map; return ChatMessageModel.fromJson(jsonResponse); } catch (e, stackTrace) { AppLogger.e('Erreur lors de l\'envoi du message', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } /// Marque un message comme lu. Future markMessageAsRead(String messageId) async { AppLogger.d('Marquer message comme lu: $messageId', tag: 'ChatRemoteDataSource'); try { final uri = Uri.parse(Urls.markMessageAsRead(messageId)); await _performRequest('PUT', uri); } catch (e, stackTrace) { AppLogger.e('Erreur lors du marquage comme lu', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } /// Marque tous les messages d'une conversation comme lus. Future markConversationAsRead(String conversationId, String userId) async { AppLogger.d('Marquer conversation comme lue: $conversationId', tag: 'ChatRemoteDataSource'); try { final uri = Uri.parse(Urls.markAllMessagesAsRead(conversationId, userId)); await _performRequest('PUT', uri); } catch (e, stackTrace) { AppLogger.e('Erreur lors du marquage de la conversation', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } /// Supprime un message. Future deleteMessage(String messageId) async { AppLogger.i('Suppression message: $messageId', tag: 'ChatRemoteDataSource'); try { final uri = Uri.parse(Urls.deleteMessage(messageId)); await _performRequest('DELETE', uri); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la suppression', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } /// Supprime une conversation. Future deleteConversation(String conversationId) async { AppLogger.i('Suppression conversation: $conversationId', tag: 'ChatRemoteDataSource'); try { final uri = Uri.parse(Urls.deleteConversation(conversationId)); await _performRequest('DELETE', uri); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la suppression de la conversation', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } /// Récupère le nombre de messages non lus pour un utilisateur. Future getUnreadMessagesCount(String userId) async { AppLogger.d('Récupération du nombre de messages non lus pour: $userId', tag: 'ChatRemoteDataSource'); try { final uri = Uri.parse(Urls.getUnreadMessagesCount(userId)); final response = await _performRequest('GET', uri); final jsonResponse = _parseJsonResponse(response, [200]) as Map; return jsonResponse['unreadCount'] as int? ?? 0; } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération du nombre de messages non lus', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource'); rethrow; } } /// Envoie un indicateur de frappe (typing indicator) via WebSocket. /// Cette méthode est maintenant gérée par le WebSocket, pas par REST. Future sendTypingIndicator(String recipientId, String userId, bool isTyping) async { AppLogger.d('Indicateur de frappe envoyé via WebSocket pour: $recipientId ($isTyping)', tag: 'ChatRemoteDataSource'); // Cette fonctionnalité sera gérée par ChatWebSocketService } }