import 'package:flutter/foundation.dart'; import '../models/installation.dart'; import 'installation_store.dart'; import 'nymea_service.dart'; /// Gère la **liste** d'installations et la **connexion active unique** au-dessus /// de [NymeaService] (qui reste mono-connexion). Aucune socket parallèle : un /// switch = déconnecter l'active proprement, puis connecter la cible. /// /// La **démo** (`startSimulation`) compte comme une connexion active factice : /// elle ne doit pas être renvoyée vers Installations par le gate (Étape 6). class ConnectionManager extends ChangeNotifier { ConnectionManager(this.service, this.store); final NymeaService service; final InstallationStore store; Installation? _active; String? _connectingToName; String? _error; /// Caches à vider au switch (providers rôles/scheduler/tariff). Enregistrés /// depuis `main()`. Découplage par callbacks — pas d'import croisé. final List _cacheClearers = []; /// Enregistre un cache à vider lors d'un changement d'installation. void registerCacheClear(void Function() clear) => _cacheClearers.add(clear); Installation? get active => _active; String? get connectingToName => _connectingToName; bool get isTransitioning => _connectingToName != null; String? get error => _error; List get installations => store.installations; /// Vrai dès qu'une session **utilisable** existe — démo, ou box réelle /// **connectée ET authentifiée**. Le transport seul (connecté mais auth en /// attente) ne compte pas : sinon le gate laisserait passer un dashboard vide. bool get hasActiveConnection => service.isSimulation || (service.connected && service.authenticated); /// Transport up mais authentification requise → router vers l'écran Connexion. bool get needsAuth => service.connected && service.authenticationRequired && !service.authenticated; /// Charge la liste persistée (à appeler au démarrage, Étape 6). Future load() async { await store.load(); notifyListeners(); } /// Bascule vers une installation connue : déconnecte l'active, connecte la /// cible avec son token (par UUID), contrôle l'identité, persiste. Future switchTo(Installation target) async { _error = null; _connectingToName = target.name; notifyListeners(); // 1. Fermer proprement l'active (WS, listeners, requêtes en attente). service.disconnect(); // 2. Invalider les caches AVANT de connecter la cible — aucune donnée de // l'ancienne box ne doit subsister (service + providers). _clearCaches(); // 3. Token par UUID (secure storage, tolérant aux pannes). final token = await store.tokenFor(target.uuid); // 4. Connexion ws://host:port (pas de TLS). final ok = await service.connect( target.host, target.port, protocol: NymeaProtocol.webSocket, token: token, ); if (!ok) { _connectingToName = null; _error = service.connectionError ?? 'Connexion échouée'; notifyListeners(); return false; } // 5. Contrôle d'identité : en DHCP, l'IP attendue peut héberger une AUTRE // box. On enregistre alors sous le vrai UUID, sans écraser la cible. final realUuid = service.serverUuid; if (realUuid.isNotEmpty && realUuid != target.uuid) { _active = await _persistFromService(host: target.host, port: target.port); _error = 'Identité différente à ${target.host} : ' '${_shortUuid(realUuid)} ≠ ${_shortUuid(target.uuid)}'; _connectingToName = null; notifyListeners(); return true; // transport up, mais sur une autre installation } // 6. Persister (host courant + lastSeen + token) sous l'UUID. _active = await _persistFromService( host: target.host, port: target.port, fallbackName: target.name, ); _connectingToName = null; notifyListeners(); return true; } /// Connecte une box **nouvelle** (ajout manuel ou découverte) : sans token. /// Si la box est déjà authentifiée (ouverte), on persiste ; sinon l'UI route /// vers l'écran Connexion (Étape 5) et l'enregistrement suit l'auth réussie. Future connectNew({required String host, int port = 4444}) async { _error = null; _connectingToName = host; notifyListeners(); service.disconnect(); _clearCaches(); final ok = await service.connect(host, port, protocol: NymeaProtocol.webSocket); if (!ok) { _connectingToName = null; _error = service.connectionError ?? 'Connexion échouée'; notifyListeners(); return false; } if (service.authenticated && service.serverUuid.isNotEmpty) { _active = await _persistFromService(host: host, port: port); } _connectingToName = null; notifyListeners(); return true; } /// Login sur la box déjà connectée (transport up, auth requise). Sur succès, /// persiste l'installation **par UUID** (token dans le secure storage). Future authenticate(String username, String password) async { _error = null; final ok = await service.authenticate(username, password); if (ok) { _active = await _persistFromService(host: service.host, port: service.port); } else { _error = service.connectionError ?? 'Identifiants refusés'; } notifyListeners(); return ok; } /// Crée l'admin d'une box neuve puis login (enchaîné côté service). Sur /// succès, persiste par UUID. **Non vérifié en live** (pas de box vierge). Future createAdmin(String username, String password, {String? email}) async { _error = null; final ok = await service.createUser(username, password, email: email); if (ok) { _active = await _persistFromService(host: service.host, port: service.port); } else { _error = service.connectionError ?? 'Création du compte refusée'; } notifyListeners(); return ok; } /// Entre en mode démo (connexion active factice). void enterDemo() { // Vider d'abord : real→démo ne doit pas garder la config de la box réelle. _clearCaches(); service.startSimulation(); _active = null; _connectingToName = null; _error = null; notifyListeners(); } /// Coupe la session active sans détruire l'enregistrement. void disconnectActive() { service.disconnect(); _active = null; notifyListeners(); } /// Supprime une installation (action installateur) + son token. Future removeInstallation(String uuid) async { await store.remove(uuid); if (_active?.uuid == uuid) { service.disconnect(); _active = null; } notifyListeners(); } // ── Interne ──────────────────────────────────────────────────────────────── /// Vide service + providers enregistrés — aucune donnée de la box précédente /// ne subsiste. void _clearCaches() { service.resetState(); for (final clear in _cacheClearers) { clear(); } } /// Construit + persiste l'installation à partir de l'identité Hello courante. /// Si l'UUID est vide (cas anormal), on ne persiste pas mais on renvoie quand /// même un enregistrement transitoire pour [active]. Future _persistFromService({ required String host, required int port, String? fallbackName, }) async { final uuid = service.serverUuid; final inst = Installation( uuid: uuid, name: service.serverName.isNotEmpty ? service.serverName : (fallbackName ?? 'nymea'), host: host, port: port, lastSeen: DateTime.now(), token: service.token.isNotEmpty ? service.token : null, ); if (uuid.isNotEmpty) await store.upsert(inst); return inst; } String _shortUuid(String u) => u.length <= 8 ? u : '${u.substring(0, 8)}…'; }