// lib/remote/remote_http.dart import 'dart:convert'; import 'dart:developer' as dev; import 'package:flutter/services.dart'; import 'remote_settings.dart'; import 'auth_client.dart'; /// Helper HTTP per risorse remote (URL + headers di autenticazione). class RemoteHttp { static RemoteAuth? _auth; static String? _base; // Cache dell'ultimo set di header (mai null: {} se non autenticato) static Map _cachedHeaders = const {}; static Future? _initFuture; // evita init concorrenti /// ✅ Inietta l'istanza di RemoteAuth usata dal controller, /// così API e immagini condividono lo stesso token/cache. static void attach({required String baseUrl, required RemoteAuth auth}) { _base = baseUrl.trim().isEmpty ? null : baseUrl.trim(); _auth = auth; _cachedHeaders = const {}; _initFuture = Future.value(); // init "risolta" dev.log('[RemoteHttp] attach: base=$_base (shared auth)', name: 'RemoteHttp'); } /// Inizializza auth e base URL dai settings sicuri (robusto: non lancia). static Future init() { _initFuture ??= _doInit(); return _initFuture!; } static Future _doInit() async { try { final s = await RemoteSettings.load(); final base = s.baseUrl.trim(); _base = base.isEmpty ? null : base; _auth = RemoteAuth(baseUrl: s.baseUrl, email: s.email, password: s.password); dev.log('[RemoteHttp] init: base=$_base, email=${s.email.isNotEmpty ? '***' : '(none)'}', name: 'RemoteHttp'); } catch (e, st) { dev.log('[RemoteHttp] init ERROR: $e', name: 'RemoteHttp', error: e, stackTrace: st); _auth = null; _base = null; } } /// ✅ Se il token JWT è quasi scaduto, forziamo refresh prima di usarlo per le immagini. static bool _isJwtExpiringSoon(String token, {Duration skew = const Duration(seconds: 60)}) { try { final parts = token.split('.'); if (parts.length < 2) return false; final payload = parts[1]; String normalized = payload.replaceAll('-', '+').replaceAll('_', '/'); while (normalized.length % 4 != 0) { normalized += '='; } final decoded = utf8.decode(base64.decode(normalized)); final map = jsonDecode(decoded); if (map is! Map) return false; final exp = map['exp']; if (exp is! num) return false; final expMs = (exp.toInt()) * 1000; final nowMs = DateTime.now().millisecondsSinceEpoch; return (expMs - nowMs) <= skew.inMilliseconds; } catch (_) { return false; } } /// Headers correnti (login on‑demand). Non lancia; ritorna {} se non disponibili. static Future> headers() async { if (_auth == null) { await init(); if (_auth == null) { _cachedHeaders = const {}; dev.log('[RemoteHttp] headers: init failed → NO TOKEN', name: 'RemoteHttp'); return _cachedHeaders; } } try { // ✅ refresh proattivo se token vicino a scadenza final tok = _auth!.token; if (tok != null && tok.isNotEmpty && _isJwtExpiringSoon(tok)) { dev.log('[RemoteHttp] token expiring soon -> refresh', name: 'RemoteHttp'); final h = await _auth!.refreshAndHeaders(); _cachedHeaders = Map.from(h); return _cachedHeaders; } final h = await _auth!.authHeaders(); // login on-demand _cachedHeaders = Map.from(h); if ((_cachedHeaders['Authorization']?.isEmpty ?? true)) { dev.log('[RemoteHttp] headers: NO Authorization', name: 'RemoteHttp'); } else { dev.log('[RemoteHttp] headers: Authorization: Bearer ***', name: 'RemoteHttp'); } return _cachedHeaders; } on PlatformException catch (e, st) { dev.log('[RemoteHttp] headers PlatformException: $e → returning {}', name: 'RemoteHttp', error: e, stackTrace: st); _cachedHeaders = const {}; return _cachedHeaders; } catch (e, st) { dev.log('[RemoteHttp] headers ERROR: $e → returning {}', name: 'RemoteHttp', error: e, stackTrace: st); _cachedHeaders = const {}; return _cachedHeaders; } } /// Ultimi header noti (sincrono). Mai null; {} se non autenticato. static Map peekHeaders() => _cachedHeaders; /// Converte path relativo in assoluto. Se è già http/https lo ritorna com’è. static String absUrl(String? relativePath) { if (relativePath == null || relativePath.isEmpty) return ''; final lp = relativePath.trim().toLowerCase(); if (lp.startsWith('http://') || lp.startsWith('https://')) { return relativePath; } if (_base == null || _base!.isEmpty) return ''; final b = _base!.endsWith('/') ? _base! : '${_base!}/'; final rel = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; return '$b$rel'; } /// Warm-up: scaldare gli header. static Future warmUp() async { await init(); try { await headers(); } catch (_) { // già loggato } } /// Rilegge i settings a runtime (es. utente cambia base/email/password). static Future refreshFromSettings() async { _initFuture = null; await init(); _cachedHeaders = const {}; } }