import 'dart:convert'; import 'package:http/http.dart' as http; import 'auth_client.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; // ------------------------- // 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>> 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; } }