// lib/auth_provider.dart import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; // Added import for post-frame callback import 'package:shared_preferences/shared_preferences.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'dart:convert'; import 'package:bcrypt/bcrypt.dart'; // Import bcrypt import 'package:flutter_secure_storage/flutter_secure_storage.dart'; // Import secure storage import 'package:environment_monitoring_app/services/api_service.dart'; import 'package:environment_monitoring_app/services/base_api_service.dart'; import 'package:environment_monitoring_app/services/server_config_service.dart'; import 'package:environment_monitoring_app/services/retry_service.dart'; import 'package:environment_monitoring_app/services/user_preferences_service.dart'; // Removed _CacheDataContainer class // Removed _loadCacheDataFromIsolate function class AuthProvider with ChangeNotifier { late final ApiService _apiService; late final DatabaseHelper _dbHelper; late final ServerConfigService _serverConfigService; late final RetryService _retryService; final UserPreferencesService _userPreferencesService = UserPreferencesService(); // Initialize secure storage final _secureStorage = const FlutterSecureStorage(); static const _passwordStorageKey = 'user_password'; // --- Session & Profile State --- String? _jwtToken; String? _userEmail; Map? _profileData; bool get isLoggedIn => _jwtToken != null; String? get userEmail => _userEmail; Map? get profileData => _profileData; // --- App State --- bool _isLoading = true; // Keep true initially bool _isFirstLogin = true; DateTime? _lastSyncTimestamp; bool _isBackgroundLoading = false; // Added flag for background loading bool get isLoading => _isLoading; bool get isBackgroundLoading => _isBackgroundLoading; bool get isFirstLogin => _isFirstLogin; DateTime? get lastSyncTimestamp => _lastSyncTimestamp; bool _isSessionExpired = false; bool get isSessionExpired => _isSessionExpired; // --- Cached Master Data --- List>? _allUsers; List>? _tarballStations; List>? _manualStations; List>? _tarballClassifications; List>? _riverManualStations; List>? _riverTriennialStations; List>? _departments; List>? _companies; List>? _positions; List>? _airClients; List>? _airManualStations; List>? _states; List>? _appSettings; List>? _npeParameterLimits; List>? _marineParameterLimits; List>? _riverParameterLimits; List>? _apiConfigs; List>? _ftpConfigs; List>? _documents; List>? _pendingRetries; // --- Getters for UI access --- List>? get allUsers => _allUsers; List>? get tarballStations => _tarballStations; List>? get manualStations => _manualStations; List>? get tarballClassifications => _tarballClassifications; List>? get riverManualStations => _riverManualStations; List>? get riverTriennialStations => _riverTriennialStations; List>? get departments => _departments; List>? get companies => _companies; List>? get positions => _positions; List>? get airClients => _airClients; List>? get airManualStations => _airManualStations; List>? get states => _states; List>? get appSettings => _appSettings; List>? get npeParameterLimits => _npeParameterLimits; List>? get marineParameterLimits => _marineParameterLimits; List>? get riverParameterLimits => _riverParameterLimits; List>? get apiConfigs => _apiConfigs; List>? get ftpConfigs => _ftpConfigs; List>? get documents => _documents; List>? get pendingRetries => _pendingRetries; // --- SharedPreferences Keys --- static const String tokenKey = 'jwt_token'; static const String userEmailKey = 'user_email'; static const String profileDataKey = 'user_profile_data'; static const String lastSyncTimestampKey = 'last_sync_timestamp'; static const String isFirstLoginKey = 'is_first_login'; static const String lastOnlineLoginKey = 'last_online_login'; AuthProvider({ required ApiService apiService, required DatabaseHelper dbHelper, required ServerConfigService serverConfigService, required RetryService retryService, }) : _apiService = apiService, _dbHelper = dbHelper, _serverConfigService = serverConfigService, _retryService = retryService { debugPrint('AuthProvider: Initializing...'); _initializeAndLoadData(); // Use the updated method name } Future isConnected() async { final connectivityResult = await Connectivity().checkConnectivity(); return !connectivityResult.contains(ConnectivityResult.none); } // Updated method using SchedulerBinding instead of compute Future _initializeAndLoadData() async { _isLoading = true; notifyListeners(); // Notify UI about initial loading state // 1. Perform quick SharedPreferences reads first. final prefs = await SharedPreferences.getInstance(); _jwtToken = prefs.getString(tokenKey); _userEmail = prefs.getString(userEmailKey); _isFirstLogin = prefs.getBool(isFirstLoginKey) ?? true; final profileJson = prefs.getString(profileDataKey); if (profileJson != null) { try { _profileData = jsonDecode(profileJson); } catch (e) { debugPrint("Failed to decode profile from prefs: $e"); prefs.remove(profileDataKey); } } // Load server config early final activeApiConfig = await _serverConfigService.getActiveApiConfig(); if (activeApiConfig == null) { debugPrint("AuthProvider: No active API config found. Setting default bootstrap URL."); final initialConfig = { 'api_config_id': 0, 'config_name': 'Default Server', 'api_url': 'https://mms-apiv4.pstw.com.my/v1', // Use actual default if needed }; await _serverConfigService.setActiveApiConfig(initialConfig); } final lastSyncString = prefs.getString(lastSyncTimestampKey); if (lastSyncString != null) { try { _lastSyncTimestamp = DateTime.parse(lastSyncString); } catch (e) { debugPrint("Error parsing last sync timestamp: $e"); prefs.remove(lastSyncTimestampKey); } } // 2. Set isLoading to false *before* scheduling heavy work. _isLoading = false; notifyListeners(); // Let the UI build // 3. Schedule heavy database load *after* the first frame. SchedulerBinding.instance.addPostFrameCallback((_) async { debugPrint("AuthProvider: First frame built. Starting background cache load..."); _isBackgroundLoading = true; // Indicate background activity notifyListeners(); // Show a secondary loading indicator if needed try { // Call the original cache loading method here await _loadDataFromCache(); debugPrint("AuthProvider: Background cache load complete."); // After loading cache, check session status and potentially sync if (_jwtToken != null) { debugPrint('AuthProvider: Session loaded.'); await _userPreferencesService.applyAndSaveDefaultPreferencesIfNeeded(); // Decide whether to call checkAndTransition or validateAndRefresh here await checkAndTransitionToOnlineSession(); // Example: Check if transition needed await validateAndRefreshSession(); // Example: Validate if already online } else { debugPrint('AuthProvider: No active session. App is in offline mode.'); } } catch (e) { debugPrint("AuthProvider: Error during background cache load: $e"); // Handle error appropriately } finally { _isBackgroundLoading = false; // Background load finished notifyListeners(); // Update UI } }); } /// Checks if the session is offline and attempts to transition to an online session by performing a silent re-login. /// Returns true if a successful transition occurred. Future checkAndTransitionToOnlineSession() async { // Condition 1: Check connectivity if (!(await isConnected())) { debugPrint("AuthProvider: No internet connection. Skipping transition check."); return false; } // Condition 2: Check if currently in an offline session state final bool inOfflineSession = _jwtToken != null && _jwtToken!.startsWith("offline-session-"); if (!inOfflineSession) { // Already online or logged out, no transition needed. // --- START: FIX FOR DOUBLE SYNC --- // Removed the redundant syncAllData call from here. if(_jwtToken != null) { debugPrint("AuthProvider: Session is already online. Skipping transition sync."); // Consider calling validateAndRefreshSession() here instead if needed, // but avoid a full syncAllData(). } // --- END: FIX FOR DOUBLE SYNC --- return false; } // Read password from secure storage final String? password = await _secureStorage.read(key: _passwordStorageKey); if (password == null || _userEmail == null) { debugPrint("AuthProvider: In offline session, but no password in secure storage for auto-relogin. Manual login required."); return false; } debugPrint("AuthProvider: Internet detected in offline session. Attempting silent re-login for $_userEmail..."); try { final result = await _apiService.login(_userEmail!, password); if (result['success'] == true) { debugPrint("AuthProvider: Silent re-login successful. Transitioning to online session."); final String token = result['data']['token']; final Map profile = result['data']['profile']; // Use existing login method to set up session and trigger sync. await login(token, profile, password); // This call includes syncAllData notifyListeners(); // Ensure UI updates after state change return true; } else { // Silent login failed debugPrint("AuthProvider: Silent re-login failed: ${result['message']}"); return false; } } catch (e) { debugPrint("AuthProvider: Silent re-login exception: $e"); return false; } } Future validateAndRefreshSession() async { if (!(await isConnected())) { debugPrint('AuthProvider: No connection, skipping session validation.'); return; } if (_isSessionExpired) { debugPrint('AuthProvider: Session is marked as expired, manual login required.'); return; } if (_jwtToken == null || _jwtToken!.startsWith("offline-session-")) { return; // No online session to validate } try { await _apiService.validateToken(); debugPrint('AuthProvider: Session token is valid.'); } on SessionExpiredException { debugPrint('AuthProvider: Session validation failed (token expired). Attempting silent re-login.'); final bool reauthenticated = await attemptSilentRelogin(); if (!reauthenticated) { debugPrint('AuthProvider: Silent re-login failed. Switching to session-expired offline mode.'); _isSessionExpired = true; notifyListeners(); } else { debugPrint('AuthProvider: Silent re-login successful. Session restored.'); } } catch (e) { debugPrint('AuthProvider: An error occurred during session validation: $e'); } } Future attemptSilentRelogin() async { if (!(await isConnected())) { debugPrint("AuthProvider: No internet for silent relogin."); return false; } final String? password = await _secureStorage.read(key: _passwordStorageKey); if (password == null || _userEmail == null) { debugPrint("AuthProvider: No cached credentials in secure storage for silent relogin."); return false; } debugPrint("AuthProvider: Session may be expired. Attempting silent re-login for $_userEmail..."); try { final result = await _apiService.login(_userEmail!, password); if (result['success'] == true) { debugPrint("AuthProvider: Silent re-login successful."); final String token = result['data']['token']; final Map profile = result['data']['profile']; // Critical: Call the main login function to update token, profile, hash, etc. // BUT prevent it from triggering another full sync immediately if called during syncAllData await _updateSessionInternals(token, profile, password); // Use helper to avoid sync loop _isSessionExpired = false; notifyListeners(); return true; } else { debugPrint("AuthProvider: Silent re-login failed: ${result['message']}"); return false; } } catch (e) { debugPrint("AuthProvider: Silent re-login exception: $e"); return false; } } // Helper to update session without triggering sync, used by attemptSilentRelogin Future _updateSessionInternals(String token, Map profile, String password) async { _jwtToken = token; _userEmail = profile['email']; await _secureStorage.write(key: _passwordStorageKey, value: password); final Map profileWithToken = Map.from(profile); profileWithToken['token'] = token; _profileData = profileWithToken; final prefs = await SharedPreferences.getInstance(); await prefs.setString(tokenKey, token); await prefs.setString(userEmailKey, _userEmail!); await prefs.setString(profileDataKey, jsonEncode(_profileData)); await prefs.setString(lastOnlineLoginKey, DateTime.now().toIso8601String()); // Update last online time await _dbHelper.saveProfile(_profileData!); try { debugPrint("AuthProvider: (Re-login) Hashing and caching password for offline login."); final String hashedPassword = await compute(hashPassword, password); await _dbHelper.upsertUserWithCredentials( profile: profile, passwordHash: hashedPassword, ); debugPrint("AuthProvider: (Re-login) Credentials cached successfully."); } catch (e) { debugPrint("AuthProvider: (Re-login) Failed to cache password hash: $e"); } // DO NOT call syncAllData here to prevent loops when called from syncAllData's catch block. } Future syncAllData({bool forceRefresh = false}) async { if (!(await isConnected())) { debugPrint("AuthProvider: Device is OFFLINE. Skipping sync."); return; } if (_isSessionExpired) { debugPrint("AuthProvider: Skipping sync, session is expired. Manual login required."); throw Exception('Session expired. Please log in again to sync.'); } if (_jwtToken == null || _jwtToken!.startsWith("offline-session-")) { debugPrint("AuthProvider: Skipping sync, session is offline or null."); return; } try { debugPrint("AuthProvider: Device is ONLINE. Starting delta sync."); final prefs = await SharedPreferences.getInstance(); final String? lastSync = forceRefresh ? null : prefs.getString(lastSyncTimestampKey); final result = await _apiService.syncAllData(lastSyncTimestamp: lastSync); if (result['success']) { debugPrint("AuthProvider: Delta sync successful."); final newSyncTimestamp = DateTime.now().toUtc().toIso8601String(); await prefs.setString(lastSyncTimestampKey, newSyncTimestamp); _lastSyncTimestamp = DateTime.parse(newSyncTimestamp); if (_isFirstLogin) { await setIsFirstLogin(false); debugPrint("AuthProvider: First successful sync complete. isFirstLogin flag set to false."); } await _loadDataFromCache(); // Reload data after successful sync notifyListeners(); } else { debugPrint("AuthProvider: Delta sync failed logically. Message: ${result['message']}"); throw Exception('Data sync failed. Please check the logs.'); } } on SessionExpiredException { debugPrint("AuthProvider: Session expired during sync. Attempting silent re-login..."); final bool reauthenticated = await attemptSilentRelogin(); if (reauthenticated) { debugPrint("AuthProvider: Re-login successful. Retrying sync..."); await syncAllData(forceRefresh: forceRefresh); // Retry the sync } else { debugPrint("AuthProvider: Re-login failed after session expired during sync. Switching to offline mode."); _isSessionExpired = true; notifyListeners(); throw Exception('Session expired. App is now in offline mode.'); } } catch (e) { debugPrint("AuthProvider: A general error occurred during sync: $e"); rethrow; } } Future syncRegistrationData() async { if (!(await isConnected())) { debugPrint("AuthProvider: Device is OFFLINE. Skipping registration data sync."); return; } debugPrint("AuthProvider: Fetching data for registration screen..."); final result = await _apiService.syncRegistrationData(); if (result['success']) { await _loadDataFromCache(); notifyListeners(); debugPrint("AuthProvider: Registration data loaded and UI notified."); } else { debugPrint("AuthProvider: Registration data sync failed."); } } Future refreshProfile() async { if (!(await isConnected())) { debugPrint("AuthProvider: Device is OFFLINE. Skipping profile refresh."); return; } if (_isSessionExpired || _jwtToken == null || _jwtToken!.startsWith("offline-session-")) { debugPrint("AuthProvider: Skipping profile refresh, session is offline, expired, or null."); return; } try { final result = await _apiService.refreshProfile(); if (result['success']) { await setProfileData(result['data']); } } on SessionExpiredException { debugPrint("AuthProvider: Session expired during profile refresh. Attempting silent re-login..."); await attemptSilentRelogin(); // Attempt re-login but don't retry refresh automatically here } catch (e) { debugPrint("AuthProvider: Error during profile refresh: $e"); } } Future proactiveTokenRefresh() async { if (!(await isConnected())) { debugPrint('AuthProvider: No connection, skipping proactive token refresh.'); return; } final prefs = await SharedPreferences.getInstance(); final lastOnlineLoginString = prefs.getString(lastOnlineLoginKey); if (lastOnlineLoginString == null) { debugPrint('AuthProvider: No last online login timestamp found, skipping proactive refresh.'); return; } try { final lastOnlineLogin = DateTime.parse(lastOnlineLoginString); if (DateTime.now().difference(lastOnlineLogin).inHours >= 24) { debugPrint('AuthProvider: Session is older than 24 hours. Attempting proactive silent re-login.'); await attemptSilentRelogin(); } else { debugPrint('AuthProvider: Session is fresh (< 24 hours old). No proactive refresh needed.'); } } catch (e) { debugPrint('AuthProvider: Error during proactive token refresh check: $e'); } } // This method performs the actual DB reads Future _loadDataFromCache() async { final prefs = await SharedPreferences.getInstance(); final profileJson = prefs.getString(profileDataKey); if (profileJson != null && _profileData == null) { try { _profileData = jsonDecode(profileJson); } catch(e) { /*...*/ } } _profileData ??= await _dbHelper.loadProfile(); _allUsers = await _dbHelper.loadUsers(); _tarballStations = await _dbHelper.loadTarballStations(); _manualStations = await _dbHelper.loadManualStations(); _tarballClassifications = await _dbHelper.loadTarballClassifications(); _riverManualStations = await _dbHelper.loadRiverManualStations(); _riverTriennialStations = await _dbHelper.loadRiverTriennialStations(); _departments = await _dbHelper.loadDepartments(); _companies = await _dbHelper.loadCompanies(); _positions = await _dbHelper.loadPositions(); _airClients = await _dbHelper.loadAirClients(); _airManualStations = await _dbHelper.loadAirManualStations(); _states = await _dbHelper.loadStates(); _appSettings = await _dbHelper.loadAppSettings(); _npeParameterLimits = await _dbHelper.loadNpeParameterLimits(); _marineParameterLimits = await _dbHelper.loadMarineParameterLimits(); _riverParameterLimits = await _dbHelper.loadRiverParameterLimits(); _documents = await _dbHelper.loadDocuments(); _apiConfigs = await _dbHelper.loadApiConfigs(); _ftpConfigs = await _dbHelper.loadFtpConfigs(); _pendingRetries = await _retryService.getPendingTasks(); // Use service here is okay debugPrint("AuthProvider: All master data loaded from local DB cache (background/sync)."); } Future refreshPendingTasks() async { _pendingRetries = await _retryService.getPendingTasks(); notifyListeners(); } Future login(String token, Map profile, String password) async { // Call the internal helper first await _updateSessionInternals(token, profile, password); // Now proceed with post-login actions that *don't* belong in the helper debugPrint('AuthProvider: Login successful. Session and profile persisted.'); await _userPreferencesService.applyAndSaveDefaultPreferencesIfNeeded(); // The main sync triggered by a direct user login await syncAllData(forceRefresh: true); // Notify listeners *after* sync is attempted (or throws) notifyListeners(); } Future loginOffline(String email, String password) async { debugPrint("AuthProvider: Attempting offline login for user $email."); try { final String? storedHash = await _dbHelper.getUserPasswordHashByEmail(email); if (storedHash == null || storedHash.isEmpty) { debugPrint("AuthProvider DEBUG: Offline login failed for $email because NO HASH was found in local storage."); return false; } debugPrint("AuthProvider: Verifying password against stored hash..."); final bool passwordMatches = await compute(verifyPassword, CheckPasswordParams(password, storedHash)); if (passwordMatches) { debugPrint("AuthProvider: Offline password verification successful."); final Map? cachedProfile = await _dbHelper.loadProfileByEmail(email); if (cachedProfile == null) { debugPrint("AuthProvider DEBUG: Offline login failed because profile data was missing, even though password matched."); return false; } _jwtToken = "offline-session-${DateTime.now().millisecondsSinceEpoch}"; _userEmail = email; await _secureStorage.write(key: _passwordStorageKey, value: password); final Map profileWithToken = Map.from(cachedProfile); profileWithToken['token'] = _jwtToken; _profileData = profileWithToken; final prefs = await SharedPreferences.getInstance(); await prefs.setString(tokenKey, _jwtToken!); await prefs.setString(userEmailKey, _userEmail!); await prefs.setString(profileDataKey, jsonEncode(_profileData)); // Load cache data immediately after offline login succeeds // This doesn't need the post-frame callback as it's triggered by user action await _loadDataFromCache(); notifyListeners(); return true; } else { debugPrint("AuthProvider DEBUG: Offline login failed because password did not match stored hash (Hash Mismatch)."); return false; } } catch (e) { debugPrint("AuthProvider DEBUG: An unexpected error occurred during offline login process: $e"); return false; } } Future setProfileData(Map data) async { final String? existingToken = _profileData?['token']; _profileData = data; if (_profileData != null && existingToken != null) { _profileData!['token'] = existingToken; } final prefs = await SharedPreferences.getInstance(); await prefs.setString(profileDataKey, jsonEncode(_profileData)); await _dbHelper.saveProfile(_profileData!); notifyListeners(); } Future setIsFirstLogin(bool value) async { _isFirstLogin = value; final prefs = await SharedPreferences.getInstance(); await prefs.setBool(isFirstLoginKey, value); notifyListeners(); } Future logout() async { debugPrint('AuthProvider: Initiating logout...'); _jwtToken = null; _userEmail = null; _profileData = null; _lastSyncTimestamp = null; _isFirstLogin = true; _isSessionExpired = false; await _secureStorage.delete(key: _passwordStorageKey); // Clear cached data _allUsers = null; _tarballStations = null; _manualStations = null; _tarballClassifications = null; _riverManualStations = null; _riverTriennialStations = null; _departments = null; _companies = null; _positions = null; _airClients = null; _airManualStations = null; _states = null; _appSettings = null; _npeParameterLimits = null; _marineParameterLimits = null; _riverParameterLimits = null; _documents = null; _apiConfigs = null; _ftpConfigs = null; _pendingRetries = null; final prefs = await SharedPreferences.getInstance(); await prefs.remove(tokenKey); await prefs.remove(userEmailKey); await prefs.remove(profileDataKey); await prefs.remove(lastSyncTimestampKey); await prefs.remove(lastOnlineLoginKey); await prefs.remove('default_preferences_saved'); // Also clear user prefs flag await prefs.setBool(isFirstLoginKey, true); debugPrint('AuthProvider: All session and cached data cleared.'); notifyListeners(); } Future> resetPassword(String email) { // Assuming _apiService has a method for this, otherwise implement it. // This looks correct based on your previous code structure. return _apiService.post('auth/forgot-password', {'email': email}); } } // These remain unchanged as they are used by compute for password hashing/checking class CheckPasswordParams { final String password; final String hash; CheckPasswordParams(this.password, this.hash); } String hashPassword(String password) { return BCrypt.hashpw(password, BCrypt.gensalt()); } bool verifyPassword(CheckPasswordParams params) { return BCrypt.checkpw(params.password, params.hash); }