import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:flutter/foundation.dart'; import 'auth_client.dart'; import 'remote_models.dart'; class RemoteHttpApi { RemoteHttpApi({ required this.baseUrl, required this.auth, this.timeout = const Duration(seconds: 20), }) : _base = Uri.parse(baseUrl.endsWith('/') ? baseUrl : '$baseUrl/'); final String baseUrl; final Uri _base; final RemoteAuth auth; final Duration timeout; /// Recupera i photo items per una lista di ids. Future> getPhotosByIds(List ids) async { if (ids.isEmpty) return []; const int chunkSize = 100; final List out = []; for (var i = 0; i < ids.length; i += chunkSize) { final end = (i + chunkSize < ids.length) ? i + chunkSize : ids.length; final chunk = ids.sublist(i, end); // prova batch endpoint try { final idsParam = chunk.map(Uri.encodeComponent).join(','); final uri = _base.resolve('photos?ids=$idsParam'); final r = await _getWithAuth(uri); final list = _decodeList(r, label: 'getPhotosByIds'); out.addAll(list.map(RemotePhotoItem.fromJson)); continue; } catch (e) { debugPrint('[remote][api] batch getPhotosByIds failed for chunk starting at $i: $e'); // fallback: prova richieste singole per questo chunk } // fallback per chunk: richieste singole for (final id in chunk) { try { final map = await getPhotoById(id); if (map != null) out.add(RemotePhotoItem.fromJson(map)); } catch (e) { debugPrint('[remote][api] getPhotosByIds fallback error for id=$id: $e'); } } } return out; } // ------------------------- // Helpers // ------------------------- Future _getWithAuth(Uri uri) async { var headers = await auth.authHeaders(); http.Response res; try { res = await http.get(uri, headers: headers).timeout(timeout); } catch (e) { throw Exception('GET fallita: errore di rete verso $uri: $e'); } // Follow redirect 307/308 se presenti (raro ma possibile) if ({307, 308}.contains(res.statusCode) && res.headers['location'] != null) { final redirectUri = uri.resolve(res.headers['location']!); try { res = await http.get(redirectUri, headers: headers).timeout(timeout); } catch (e) { throw Exception('GET fallita: errore di rete verso $redirectUri: $e'); } } // Se token scaduto -> refresh e retry una sola volta if (res.statusCode == 401) { headers = await auth.refreshAndHeaders(); try { res = await http.get(uri, headers: headers).timeout(timeout); } catch (e) { throw Exception('GET fallita dopo refresh token verso $uri: $e'); } } // Gestione errori HTTP if (res.statusCode < 200 || res.statusCode >= 300) { final snippet = utf8.decode(res.bodyBytes.take(300).toList()); throw Exception('HTTP ${res.statusCode} ${res.reasonPhrase} su $uri – $snippet'); } return res; } List> _decodeList(http.Response r, {required String label}) { final decoded = jsonDecode(utf8.decode(r.bodyBytes)); if (decoded is! List) { throw Exception('$label: risposta non è una lista JSON'); } return decoded.cast>(); } // ------------------------- // API // ------------------------- Future sendSyncDone({ required String sessionId, required String deviceId, required int lastSyncMs, }) async { final uri = _base.resolve('photos/sync_done'); final headers = await auth.authHeaders(); headers['Content-Type'] = 'application/json'; final body = jsonEncode({ 'session_id': sessionId, 'device_id': deviceId, 'last_sync': lastSyncMs, }); final res = await http .post(uri, headers: headers, body: body) .timeout(timeout); if (res.statusCode < 200 || res.statusCode >= 300) { final snippet = utf8.decode(res.bodyBytes.take(300).toList()); throw Exception('sync_done: HTTP ${res.statusCode} – $snippet'); } } Future>> getAllPhotos() async { final uri = _base.resolve('photos'); final r = await _getWithAuth(uri); return _decodeList(r, label: 'getAllPhotos'); } Future>> getChanges(String sinceIso) async { final uri = _base.resolve('photos/changes?since=${Uri.encodeComponent(sinceIso)}'); final r = await _getWithAuth(uri); return _decodeList(r, label: 'getChanges'); } Future>> getDeletedHard(String sinceIso) async { final uri = _base.resolve('photos/deleted_hard?since=${Uri.encodeComponent(sinceIso)}'); final r = await _getWithAuth(uri); final decoded = jsonDecode(utf8.decode(r.bodyBytes)); if (decoded is! Map) { throw Exception('getDeletedHard: risposta non è un oggetto JSON'); } final deleted = (decoded['deleted'] ?? []) as List; return deleted.cast>(); } Future?> getPhotoById(String id) async { final uri = _base.resolve('photos/$id'); final r = await _getWithAuth(uri); final decoded = jsonDecode(utf8.decode(r.bodyBytes)); if (decoded is List && decoded.isNotEmpty) { final first = decoded.first; if (first is Map) return first; throw Exception('getPhotoById: primo elemento non è un oggetto JSON'); } return null; } }