import 'package:firebase_auth/firebase_auth.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:em2rp/config/api_config.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/utils/debug_log.dart'; /// Interface abstraite pour les opérations API /// Permet de changer facilement de backend (Firebase Functions, REST API personnalisé, etc.) abstract class ApiService { Future> call(String functionName, Map data); Future get(String endpoint, {Map? params}); Future post(String endpoint, Map data); Future put(String endpoint, Map data); Future delete(String endpoint, {Map? data}); } /// Implémentation pour Firebase Cloud Functions class FirebaseFunctionsApiService implements ApiService { // URL de base - gérée par ApiConfig String get _baseUrl => ApiConfig.baseUrl; /// Récupère le token d'authentification Firebase Future _getAuthToken() async { final user = FirebaseAuth.instance.currentUser; if (user == null) return null; return await user.getIdToken(); } /// Headers par défaut avec authentification Future> _getHeaders() async { final token = await _getAuthToken(); return { 'Content-Type': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }; } /// Convertit récursivement TOUT en types JSON standards (String, num, bool, List, Map) /// Garantit que toutes les Maps sont des Map littérales dynamic _toJsonSafe(dynamic value) { if (value == null) return null; // Types primitifs JSON-safe if (value is String || value is num || value is bool) { return value; } // Types Firestore if (value is Timestamp) { return value.toDate().toIso8601String(); } if (value is DateTime) { return value.toIso8601String(); } if (value is DocumentReference) { return value.path; } if (value is GeoPoint) { // Créer une Map littérale explicite return { 'latitude': value.latitude, 'longitude': value.longitude, }; } // Listes - créer une nouvelle List littérale if (value is List) { final result = []; for (final item in value) { result.add(_toJsonSafe(item)); } return result; } // Maps - créer une nouvelle Map littérale explicite if (value is Map) { final result = {}; value.forEach((k, v) { final key = k.toString(); final convertedValue = _toJsonSafe(v); result[key] = convertedValue; }); return result; } // Type non supporté - retourner en String return value.toString(); } /// Prépare les données pour jsonEncode en faisant un double passage Map _prepareForJson(Map data) { try { // Premier passage : convertir tous les types Firestore final safeData = _toJsonSafe(data); // Deuxième passage : encoder puis décoder pour forcer la normalisation // Cela garantit que tout est 100% compatible JSON et élimine tous les _JsonMap final jsonString = jsonEncode(safeData); final decoded = jsonDecode(jsonString); // Force le type Map if (decoded is Map) { return Map.from(decoded); } // Fallback - ne devrait jamais arriver return Map.from(safeData as Map); } catch (e) { // Si l'encodage échoue, essayer de créer une copie profonde manuelle DebugLog.error('[API] Error in _prepareForJson', e); DebugLog.info('[API] Trying manual deep copy...'); return _deepCopyMap(data); } } /// Copie profonde manuelle d'une Map pour éviter les _JsonMap Map _deepCopyMap(Map source) { final result = {}; source.forEach((key, value) { if (value is Map) { result[key] = _deepCopyMap(Map.from(value)); } else if (value is List) { result[key] = _deepCopyList(value); } else { result[key] = value; } }); return result; } /// Copie profonde manuelle d'une List List _deepCopyList(List source) { return source.map((item) { if (item is Map) { return _deepCopyMap(Map.from(item)); } else if (item is List) { return _deepCopyList(item); } else { return item; } }).toList(); } @override Future> call(String functionName, Map data) async { final url = Uri.parse('$_baseUrl/$functionName'); final headers = await _getHeaders(); // Préparer les données avec double passage pour éviter les _JsonMap final preparedData = _prepareForJson(data); // Log pour débogage (seulement en mode debug) DebugLog.info('[API] Calling $functionName with eventId: ${preparedData['eventId']}'); try { // Encoder directement avec jsonEncode standard final bodyJson = jsonEncode({'data': preparedData}); final response = await http.post( url, headers: headers, body: bodyJson, ); if (response.statusCode >= 200 && response.statusCode < 300) { final responseData = jsonDecode(response.body); return responseData is Map ? responseData : {}; } else { final error = jsonDecode(response.body); throw ApiException( message: error['error'] ?? 'Unknown error', statusCode: response.statusCode, ); } } catch (e) { DebugLog.error('[API] Error during request: $functionName', e); throw ApiException( message: 'Error calling $functionName: $e', statusCode: 0, ); } } @override Future get(String endpoint, {Map? params}) async { final url = Uri.parse('$_baseUrl/$endpoint').replace(queryParameters: params); final headers = await _getHeaders(); final response = await http.get(url, headers: headers); if (response.statusCode >= 200 && response.statusCode < 300) { final responseData = jsonDecode(response.body); return responseData as T?; } else if (response.statusCode == 404) { return null; } else { final error = jsonDecode(response.body); throw ApiException( message: error['error'] ?? 'Unknown error', statusCode: response.statusCode, ); } } @override Future post(String endpoint, Map data) async { final url = Uri.parse('$_baseUrl/$endpoint'); final headers = await _getHeaders(); // Préparer les données avec double passage final preparedData = _prepareForJson(data); final response = await http.post( url, headers: headers, body: jsonEncode({'data': preparedData}), ); if (response.statusCode >= 200 && response.statusCode < 300) { final responseData = jsonDecode(response.body); return responseData as T; } else { final error = jsonDecode(response.body); throw ApiException( message: error['error'] ?? 'Unknown error', statusCode: response.statusCode, ); } } @override Future put(String endpoint, Map data) async { final url = Uri.parse('$_baseUrl/$endpoint'); final headers = await _getHeaders(); // Préparer les données avec double passage final preparedData = _prepareForJson(data); final response = await http.put( url, headers: headers, body: jsonEncode({'data': preparedData}), ); if (response.statusCode >= 200 && response.statusCode < 300) { final responseData = jsonDecode(response.body); return responseData as T; } else { final error = jsonDecode(response.body); throw ApiException( message: error['error'] ?? 'Unknown error', statusCode: response.statusCode, ); } } @override Future delete(String endpoint, {Map? data}) async { final url = Uri.parse('$_baseUrl/$endpoint'); final headers = await _getHeaders(); // Préparer les données avec double passage si data existe final preparedData = data != null ? _prepareForJson(data) : null; final response = await http.delete( url, headers: headers, body: preparedData != null ? jsonEncode({'data': preparedData}) : null, ); if (response.statusCode < 200 || response.statusCode >= 300) { final error = jsonDecode(response.body); throw ApiException( message: error['error'] ?? 'Unknown error', statusCode: response.statusCode, ); } } } /// Exception personnalisée pour les erreurs API class ApiException implements Exception { final String message; final int statusCode; ApiException({ required this.message, required this.statusCode, }); @override String toString() => 'ApiException($statusCode): $message'; bool get isForbidden => statusCode == 403; bool get isUnauthorized => statusCode == 401; bool get isNotFound => statusCode == 404; bool get isConflict => statusCode == 409; } /// Instance singleton du service API final ApiService apiService = FirebaseFunctionsApiService();