import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'simple_e2ee_service.dart'; import 'local_key_backup_service.dart'; import 'local_message_store.dart'; import 'auth_service.dart'; /// Manages automatic encrypted cloud backups of chat data. /// /// Flow: User enables backup → sets password once → chats auto-backup. /// To restore on another device, user enters the same password. class ChatBackupManager { static final ChatBackupManager _instance = ChatBackupManager._internal(); static ChatBackupManager get instance => _instance; ChatBackupManager._internal(); static const String _passwordKey = 'chat_backup_password_v1'; static const String _enabledKey = 'chat_backup_enabled'; static const String _lastBackupKey = 'chat_backup_last_at'; static const String _lastBackupCountKey = 'chat_backup_last_msg_count'; static const Duration _minBackupInterval = Duration(minutes: 10); static const int _minNewMessages = 1; final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); final SimpleE2EEService _e2ee = SimpleE2EEService(); Timer? _debounceTimer; bool _backupInProgress = false; DateTime? _lastBackupAt; int _lastBackupMessageCount = 0; bool? _enabledCache; /// Whether backup has been set up (password stored + enabled) Future get isEnabled async { if (_enabledCache != null) return _enabledCache!; final prefs = await SharedPreferences.getInstance(); _enabledCache = prefs.getBool(_enabledKey) ?? false; return _enabledCache!; } /// Whether a backup password has been configured Future get hasPassword async { final pw = await _secureStorage.read(key: _passwordKey); return pw != null && pw.isNotEmpty; } /// Last backup timestamp Future get lastBackupAt async { if (_lastBackupAt != null) return _lastBackupAt; final prefs = await SharedPreferences.getInstance(); final ms = prefs.getInt(_lastBackupKey); if (ms != null) { _lastBackupAt = DateTime.fromMillisecondsSinceEpoch(ms); } return _lastBackupAt; } /// Number of messages in last backup Future get lastBackupMessageCount async { if (_lastBackupMessageCount > 0) return _lastBackupMessageCount; final prefs = await SharedPreferences.getInstance(); _lastBackupMessageCount = prefs.getInt(_lastBackupCountKey) ?? 0; return _lastBackupMessageCount; } /// Set up backup: store password and enable auto-backup. /// Called once during the setup flow. Future enable(String password) async { await _secureStorage.write(key: _passwordKey, value: password); final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_enabledKey, true); _enabledCache = true; // Perform initial backup immediately await _performBackup(); } /// Disable auto-backup and clear stored password. Future disable() async { await _secureStorage.delete(key: _passwordKey); final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_enabledKey, false); _enabledCache = false; _debounceTimer?.cancel(); } /// Change the backup password. Re-encrypts and uploads immediately. Future changePassword(String newPassword) async { await _secureStorage.write(key: _passwordKey, value: newPassword); await _performBackup(); } /// Schedule a backup after a debounce period. /// Call this after sending/receiving messages. void scheduleBackup() { _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(seconds: 30), () { _tryAutoBackup(); }); } /// Trigger an immediate backup if conditions are met. /// Called by SyncManager after sync, or on app background. Future triggerBackupIfNeeded() async { await _tryAutoBackup(); } /// Force an immediate backup regardless of schedule. Future forceBackup() async { await _performBackup(); } /// Restore from cloud backup using the given password. /// Returns restore result with counts. Future> restoreFromCloud(String password) async { return await LocalKeyBackupService.restoreFromCloud( password: password, e2eeService: _e2ee, ); } /// Get current local message count for status display. Future getLocalMessageCount() async { final records = await LocalMessageStore.instance.getAllMessageRecords(); return records.length; } /// Clear all local message data. Future clearLocalMessages() async { await LocalMessageStore.instance.clearAll(); } /// Reset everything on sign-out. void reset() { _debounceTimer?.cancel(); _lastBackupAt = null; _lastBackupMessageCount = 0; _enabledCache = null; _backupInProgress = false; } // --- Private --- Future _tryAutoBackup() async { if (_backupInProgress) return; if (!AuthService.instance.isAuthenticated) return; final enabled = await isEnabled; if (!enabled) return; final hasPw = await hasPassword; if (!hasPw) return; // Check minimum interval final last = await lastBackupAt; if (last != null && DateTime.now().difference(last) < _minBackupInterval) { return; } // Check if there are new messages since last backup final currentCount = await getLocalMessageCount(); final lastCount = await lastBackupMessageCount; if (currentCount <= 0) return; if (currentCount - lastCount < _minNewMessages && last != null) return; await _performBackup(); } Future _performBackup() async { if (_backupInProgress) return; _backupInProgress = true; try { final password = await _secureStorage.read(key: _passwordKey); if (password == null || password.isEmpty) return; if (!_e2ee.isReady) { await _e2ee.initialize(); if (!_e2ee.isReady) return; } final backup = await LocalKeyBackupService.createEncryptedBackup( password: password, e2eeService: _e2ee, includeMessages: true, includeKeys: true, ); await LocalKeyBackupService.uploadToCloud(backup: backup); // Record success final now = DateTime.now(); _lastBackupAt = now; final msgCount = await getLocalMessageCount(); _lastBackupMessageCount = msgCount; final prefs = await SharedPreferences.getInstance(); await prefs.setInt(_lastBackupKey, now.millisecondsSinceEpoch); await prefs.setInt(_lastBackupCountKey, msgCount); debugPrint('[ChatBackup] Auto-backup complete: $msgCount messages'); } catch (e) { debugPrint('[ChatBackup] Auto-backup failed: $e'); } finally { _backupInProgress = false; } } }