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/user_model.dart'; /// Source de données distante pour les opérations liées aux utilisateurs. /// /// Cette classe gère les appels API pour l'authentification, la récupération, /// la création, la mise à jour et la suppression des utilisateurs. /// Elle inclut une gestion robuste des erreurs et des logs détaillés. class UserRemoteDataSource { /// Constructeur avec injection du client HTTP. UserRemoteDataSource(this.client); /// Client HTTP utilisé pour les requêtes réseau. final http.Client client; /// Exécute une requête HTTP générique avec gestion des erreurs et des logs. /// /// [method] : La méthode HTTP (GET, POST, PUT, DELETE, PATCH). /// [uri] : L'URI complète de la requête. /// [headers] : Les en-têtes de la requête (optionnel). /// [body] : Le corps de la requête (optionnel). /// /// Retourne la réponse HTTP. /// Lève une [ServerException] ou [SocketException] en cas d'erreur. Future _performRequest( String method, Uri uri, { Map? headers, Object? body, }) async { if (EnvConfig.enableDetailedLogs) { AppLogger.http(method, uri.toString()); AppLogger.d('En-têtes: $headers', tag: 'UserRemoteDataSource'); AppLogger.d('Corps: $body', tag: 'UserRemoteDataSource'); } try { http.Response response; switch (method) { case 'GET': response = await client.get(uri, headers: headers).timeout( Duration(seconds: EnvConfig.networkTimeout), ); break; case 'POST': response = await client .post(uri, headers: headers, body: body) .timeout(Duration(seconds: EnvConfig.networkTimeout)); break; case 'PUT': response = await client .put(uri, headers: headers, body: body) .timeout(Duration(seconds: EnvConfig.networkTimeout)); break; case 'DELETE': response = await client.delete(uri, headers: headers).timeout( Duration(seconds: EnvConfig.networkTimeout), ); break; case 'PATCH': response = await client .patch(uri, headers: headers, body: body) .timeout(Duration(seconds: EnvConfig.networkTimeout)); break; default: throw ArgumentError('Méthode HTTP non supportée: $method'); } if (EnvConfig.enableDetailedLogs) { AppLogger.http(method, uri.toString(), statusCode: response.statusCode); AppLogger.d('Réponse: ${response.body}', tag: 'UserRemoteDataSource'); } return response; } on SocketException catch (e, stackTrace) { AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'UserRemoteDataSource'); 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: 'UserRemoteDataSource'); throw ServerException('Erreur client HTTP: ${e.message}'); } on FormatException catch (e, stackTrace) { AppLogger.e('Erreur de format de réponse JSON', error: e, stackTrace: stackTrace, tag: 'UserRemoteDataSource'); throw const ServerException('Réponse du serveur mal formatée.'); } on HandshakeException catch (e, stackTrace) { AppLogger.e('Erreur de handshake SSL/TLS', error: e, stackTrace: stackTrace, tag: 'UserRemoteDataSource'); throw const ServerException('Erreur de sécurité: Problème de certificat SSL/TLS.'); } catch (e, stackTrace) { AppLogger.e('Erreur inattendue lors de la requête', error: e, stackTrace: stackTrace, tag: 'UserRemoteDataSource'); rethrow; // Rethrow other unexpected exceptions } } /// Parse la réponse JSON et gère les codes de statut HTTP. /// /// [response] : La réponse HTTP à parser. /// [expectedStatusCodes] : Liste des codes de statut HTTP attendus pour une réponse réussie. /// /// Retourne le corps de la réponse décodé. /// Lève des exceptions spécifiques en fonction du code de statut. dynamic _parseJsonResponse( http.Response response, List expectedStatusCodes, ) { if (expectedStatusCodes.contains(response.statusCode)) { if (response.body.isEmpty) { return {}; // Retourne un objet vide pour les réponses 204 No Content } return json.decode(response.body); } else { final String errorMessage = json.decode(response.body)['message'] as String? ?? 'Erreur serveur inconnue'; AppLogger.e('Erreur API (${response.statusCode}): $errorMessage', tag: 'UserRemoteDataSource'); switch (response.statusCode) { case 401: throw UnauthorizedException(errorMessage); case 404: throw UserNotFoundException(errorMessage); case 409: throw ConflictException(errorMessage); default: throw ServerException( 'Erreur serveur (${response.statusCode}): $errorMessage', ); } } } /// Authentifie un utilisateur avec l'email et le mot de passe. /// /// [email] : L'email de l'utilisateur. /// [password] : Le mot de passe de l'utilisateur. /// /// Retourne un [UserModel] si l'authentification réussit. /// Lève une [UnauthorizedException] si les identifiants sont incorrects. /// Lève une [ServerException] pour d'autres erreurs serveur. Future authenticateUser(String email, String password) async { final uri = Uri.parse(Urls.authenticateUser); final headers = {'Content-Type': 'application/json'}; final body = jsonEncode({'email': email, 'motDePasse': password}); final response = await _performRequest('POST', uri, headers: headers, body: body); final userData = _parseJsonResponse(response, [200]) as Map; if (userData['userId'] != null && userData['userId'].isNotEmpty) { return UserModel.fromJson(userData); } else { throw const ServerException('ID utilisateur manquant dans la réponse.'); } } /// Récupère un utilisateur par son identifiant. /// /// [id] : L'identifiant unique de l'utilisateur. /// /// Retourne un [UserModel] si l'utilisateur est trouvé. /// Lève une [UserNotFoundException] si l'utilisateur n'existe pas. /// Lève une [ServerException] pour d'autres erreurs serveur. Future getUser(String id) async { final uri = Uri.parse(Urls.getUserByIdWithId(id)); final response = await _performRequest('GET', uri); final jsonResponse = _parseJsonResponse(response, [200]) as Map; return UserModel.fromJson(jsonResponse); } /// Recherche un utilisateur par email. /// /// [email] : L'email de l'utilisateur à rechercher. /// /// Retourne un [UserModel] si l'utilisateur est trouvé. /// Lève une [UserNotFoundException] si l'utilisateur n'existe pas. /// Lève une [ServerException] pour d'autres erreurs serveur. Future searchUserByEmail(String email) async { final uri = Uri.parse(Urls.searchUserByEmail(email)); final response = await _performRequest('GET', uri); if (response.statusCode == 404) { throw UserNotFoundException('Utilisateur non trouvé avec l\'email : $email'); } final jsonResponse = _parseJsonResponse(response, [200]) as Map; return UserModel.fromJson(jsonResponse); } /// Crée un nouvel utilisateur dans le backend. /// /// [user] : Le [UserModel] à créer. /// /// Retourne le [UserModel] créé avec les données du serveur. /// Lève une [ConflictException] si un utilisateur avec le même email existe déjà. /// Lève une [ServerException] pour d'autres erreurs serveur. Future createUser(UserModel user) async { final uri = Uri.parse(Urls.createUser); final headers = {'Content-Type': 'application/json'}; final body = jsonEncode(user.toJson()); final response = await _performRequest('POST', uri, headers: headers, body: body); final jsonResponse = _parseJsonResponse(response, [201]) as Map; return UserModel.fromJson(jsonResponse); } /// Met à jour un utilisateur existant. /// /// [user] : Le [UserModel] avec les données mises à jour. /// /// Retourne le [UserModel] mis à jour avec les données du serveur. /// Lève une [UserNotFoundException] si l'utilisateur n'existe pas. /// Lève une [ServerException] pour d'autres erreurs serveur. Future updateUser(UserModel user) async { final uri = Uri.parse(Urls.updateUserWithId(user.userId)); final headers = {'Content-Type': 'application/json'}; final body = jsonEncode(user.toJson()); final response = await _performRequest('PUT', uri, headers: headers, body: body); final jsonResponse = _parseJsonResponse(response, [200]) as Map; return UserModel.fromJson(jsonResponse); } /// Supprime un utilisateur par son identifiant. /// /// [id] : L'identifiant unique de l'utilisateur à supprimer. /// /// Ne retourne rien en cas de succès. /// Lève une [ServerException] pour d'autres erreurs serveur. Future deleteUser(String id) async { final uri = Uri.parse(Urls.deleteUserWithId(id)); final response = await _performRequest('DELETE', uri); _parseJsonResponse(response, [204]); // 204 No Content } /// Demande la réinitialisation du mot de passe. /// /// [email] : L'email de l'utilisateur qui souhaite réinitialiser son mot de passe. /// /// Ne retourne rien en cas de succès. /// Lève une [UserNotFoundException] si l'utilisateur n'existe pas. /// Lève une [ServerException] pour d'autres erreurs serveur. /// /// **Note:** Le backend actuel ne supporte pas encore cette fonctionnalité. /// Cette méthode est préparée pour une future implémentation. Future requestPasswordReset(String email) async { // TODO: Implémenter quand l'endpoint sera disponible dans le backend // Le backend actuel a seulement /users/{id}/reset-password qui nécessite l'ID throw const ServerException( 'La réinitialisation du mot de passe par email n\'est pas encore disponible. ' 'Contactez l\'administrateur.', ); } /// Réinitialise le mot de passe d'un utilisateur par son ID. /// /// [userId] : L'ID de l'utilisateur. /// [newPassword] : Le nouveau mot de passe. /// /// Ne retourne rien en cas de succès. /// Lève une [UserNotFoundException] si l'utilisateur n'existe pas. /// Lève une [ServerException] pour d'autres erreurs serveur. Future resetPasswordById(String userId, String newPassword) async { final uri = Uri.parse('${Urls.getUserByIdWithId(userId)}/reset-password?newPassword=${Uri.encodeComponent(newPassword)}'); final response = await _performRequest('PATCH', uri); _parseJsonResponse(response, [200, 204]); } }