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/story_model.dart'; /// Source de données distante pour les stories. /// /// Cette classe gère toutes les opérations liées aux stories /// via l'API backend. Elle inclut la gestion d'erreurs, les timeouts, /// et la validation des réponses. class StoryRemoteDataSource { /// Crée une nouvelle instance de [StoryRemoteDataSource]. StoryRemoteDataSource(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 PUBLIQUES // ============================================================================ /// Récupère toutes les stories actives. /// /// [viewerId] ID de l'utilisateur actuel (optionnel) pour marquer les stories vues Future> getStories({String? viewerId}) async { AppLogger.d('Récupération de toutes les stories actives', tag: 'StoryRemoteDataSource'); try { var uri = Uri.parse(Urls.getAllStories); if (viewerId != null) { uri = Uri.parse('${Urls.getAllStories}?viewerId=$viewerId'); } final response = await client .get(uri, headers: _defaultHeaders) .timeout(_timeout); return _handleListResponse(response); } on SocketException catch (e, stackTrace) { AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource'); 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: 'StoryRemoteDataSource'); throw ServerException('Erreur client HTTP: ${e.message}'); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des stories', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource'); rethrow; } } /// Récupère les stories d'un utilisateur spécifique. /// /// [userId] L'ID de l'utilisateur /// [viewerId] ID de l'utilisateur actuel (optionnel) Future> getStoriesByUserId( String userId, { String? viewerId, }) async { AppLogger.d('Récupération des stories pour l\'utilisateur: $userId', tag: 'StoryRemoteDataSource'); try { var uri = Uri.parse(Urls.getStoriesByUserId(userId)); if (viewerId != null) { uri = Uri.parse('${Urls.getStoriesByUserId(userId)}?viewerId=$viewerId'); } final response = await client .get(uri, headers: _defaultHeaders) .timeout(_timeout); return _handleListResponse(response); } on SocketException catch (e, stackTrace) { AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource'); throw const ServerException( 'Erreur de connexion réseau. Vérifiez votre connexion internet.', ); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la récupération des stories par utilisateur', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource'); rethrow; } } /// Crée une nouvelle story. /// /// [userId] L'ID de l'utilisateur créateur /// [mediaType] Le type de média (IMAGE ou VIDEO) /// [mediaUrl] L'URL du média /// [thumbnailUrl] L'URL du thumbnail (optionnel, pour vidéos) /// [durationSeconds] Durée en secondes (optionnel, pour vidéos) Future createStory({ required String userId, required String mediaType, required String mediaUrl, String? thumbnailUrl, int? durationSeconds, }) async { AppLogger.i('Création d\'une story', tag: 'StoryRemoteDataSource'); try { final body = jsonEncode({ 'userId': userId, 'mediaType': mediaType, 'mediaUrl': mediaUrl, if (thumbnailUrl != null) 'thumbnailUrl': thumbnailUrl, if (durationSeconds != null) 'durationSeconds': durationSeconds, }); final response = await client .post( Uri.parse(Urls.createStory), headers: _defaultHeaders, body: body, ) .timeout(_timeout); return _handleSingleResponse(response); } on SocketException catch (e, stackTrace) { AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource'); throw const ServerException( 'Erreur de connexion réseau. Vérifiez votre connexion internet.', ); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la création de la story', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource'); rethrow; } } /// Marque une story comme vue. /// /// [storyId] L'ID de la story /// [viewerId] L'ID de l'utilisateur qui voit la story Future markStoryAsViewed(String storyId, String viewerId) async { AppLogger.d('Marquage de la story $storyId comme vue', tag: 'StoryRemoteDataSource'); try { final response = await client .post( Uri.parse(Urls.markStoryAsViewedWithId(storyId, viewerId)), headers: _defaultHeaders, ) .timeout(_timeout); return _handleSingleResponse(response); } on SocketException catch (e, stackTrace) { AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource'); throw const ServerException( 'Erreur de connexion réseau. Vérifiez votre connexion internet.', ); } catch (e, stackTrace) { AppLogger.e('Erreur lors du marquage comme vue', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource'); rethrow; } } /// Supprime une story. /// /// [storyId] L'ID de la story Future deleteStory(String storyId) async { AppLogger.i('Suppression de la story $storyId', tag: 'StoryRemoteDataSource'); try { final response = await client .delete( Uri.parse(Urls.deleteStoryWithId(storyId)), headers: _defaultHeaders, ) .timeout(_timeout); if (response.statusCode != 200 && response.statusCode != 204) { throw ServerException( 'Erreur lors de la suppression de la story. Code: ${response.statusCode}', ); } AppLogger.i('Story supprimée avec succès', tag: 'StoryRemoteDataSource'); } on SocketException catch (e, stackTrace) { AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource'); throw const ServerException( 'Erreur de connexion réseau. Vérifiez votre connexion internet.', ); } catch (e, stackTrace) { AppLogger.e('Erreur lors de la suppression', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource'); rethrow; } } // ============================================================================ // MÉTHODES PRIVÉES UTILITAIRES // ============================================================================ /// Gère la réponse pour une liste de stories. List _handleListResponse(http.Response response) { AppLogger.http('GET', 'stories', statusCode: response.statusCode); AppLogger.d('Response body: ${response.body}', tag: 'StoryRemoteDataSource'); if (response.statusCode == 200) { final List jsonList = jsonDecode(response.body) as List; return jsonList .map((json) => StoryModel.fromJson(json as Map)) .toList(); } else if (response.statusCode == 404) { throw const ServerException('Stories non trouvées.'); } else { throw ServerException( 'Erreur lors de la récupération des stories. Code: ${response.statusCode}', ); } } /// Gère la réponse pour une seule story. StoryModel _handleSingleResponse(http.Response response) { AppLogger.http('POST', 'story', statusCode: response.statusCode); AppLogger.d('Response body: ${response.body}', tag: 'StoryRemoteDataSource'); if (response.statusCode == 200 || response.statusCode == 201) { final Map json = jsonDecode(response.body) as Map; return StoryModel.fromJson(json); } else if (response.statusCode == 404) { throw const ServerException('Story non trouvée.'); } else { throw ServerException( 'Erreur lors de l'opération sur la story. Code: ${response.statusCode}', ); } } }