import 'dart:convert'; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'models/camera_profile.dart'; import 'models/pending_upload.dart'; import 'services/auth_service.dart'; import 'services/uploader.dart'; import 'services/profile_service.dart'; // Enum for upload mode (Production, OSM Sandbox, Simulate) enum UploadMode { production, sandbox, simulate } // ------------------ AddCameraSession ------------------ class AddCameraSession { AddCameraSession({required this.profile, this.directionDegrees = 0}); CameraProfile profile; double directionDegrees; LatLng? target; } // ------------------ AppState ------------------ class AppState extends ChangeNotifier { AppState() { _init(); } final _auth = AuthService(); String? _username; final List _profiles = []; final Set _enabled = {}; static const String _enabledPrefsKey = 'enabled_profiles'; // Upload mode: production, sandbox, or simulate (in-memory, no uploads) UploadMode _uploadMode = UploadMode.production; static const String _uploadModePrefsKey = 'upload_mode'; UploadMode get uploadMode => _uploadMode; Future setUploadMode(UploadMode mode) async { _uploadMode = mode; // Update AuthService to match new mode _auth.setUploadMode(mode); // Refresh user display for active mode, validating token try { if (await _auth.isLoggedIn()) { print('AppState: Switching mode, token exists; validating...'); final isValid = await validateToken(); if (isValid) { print('AppState: Switching mode; fetching username for $mode...'); _username = await _auth.login(); if (_username != null) { print('AppState: Switched mode, now logged in as $_username'); } else { print('AppState: Switched mode but failed to retrieve username'); } } else { print('AppState: Switching mode, token invalid—auto-logout.'); await logout(); // This clears _username also. } } else { _username = null; print('AppState: Mode change: not logged in in $mode'); } } catch (e) { _username = null; print('AppState: Mode change user restoration error: $e'); } final prefs = await SharedPreferences.getInstance(); await prefs.setInt(_uploadModePrefsKey, mode.index); print('AppState: Upload mode set to $mode'); notifyListeners(); } // For legacy bool test mode static const String _legacyTestModePrefsKey = 'test_mode'; AddCameraSession? _session; AddCameraSession? get session => _session; final List _queue = []; Timer? _uploadTimer; bool get isLoggedIn => _username != null; String get username => _username ?? ''; // ---------- Init ---------- Future _init() async { // Initialize profiles: built-in + custom _profiles.add(CameraProfile.alpr()); _profiles.addAll(await ProfileService().load()); // Load enabled profile IDs and upload/test mode from prefs final prefs = await SharedPreferences.getInstance(); final enabledIds = prefs.getStringList(_enabledPrefsKey); if (enabledIds != null && enabledIds.isNotEmpty) { // Restore enabled profiles by id _enabled.addAll(_profiles.where((p) => enabledIds.contains(p.id))); } else { // By default, all are enabled _enabled.addAll(_profiles); } // Upload mode loading (including migration from old test_mode bool) if (prefs.containsKey(_uploadModePrefsKey)) { final idx = prefs.getInt(_uploadModePrefsKey) ?? 0; if (idx >= 0 && idx < UploadMode.values.length) { _uploadMode = UploadMode.values[idx]; } } else if (prefs.containsKey(_legacyTestModePrefsKey)) { // migrate legacy test_mode (true->simulate, false->prod) final legacy = prefs.getBool(_legacyTestModePrefsKey) ?? false; _uploadMode = legacy ? UploadMode.simulate : UploadMode.production; await prefs.remove(_legacyTestModePrefsKey); await prefs.setInt(_uploadModePrefsKey, _uploadMode.index); } // Ensure AuthService follows loaded mode _auth.setUploadMode(_uploadMode); await _loadQueue(); // Check if we're already logged in and get username try { if (await _auth.isLoggedIn()) { print('AppState: User appears to be logged in, fetching username...'); _username = await _auth.login(); if (_username != null) { print('AppState: Successfully retrieved username: $_username'); } else { print('AppState: Failed to retrieve username despite being logged in'); } } else { print('AppState: User is not logged in'); } } catch (e) { print('AppState: Error during auth initialization: $e'); } _startUploader(); notifyListeners(); } // ---------- Auth ---------- Future login() async { try { print('AppState: Starting login process...'); _username = await _auth.login(); if (_username != null) { print('AppState: Login successful for user: $_username'); } else { print('AppState: Login failed - no username returned'); } } catch (e) { print('AppState: Login error: $e'); _username = null; } notifyListeners(); } Future logout() async { await _auth.logout(); _username = null; notifyListeners(); } // Add method to refresh auth state Future refreshAuthState() async { try { print('AppState: Refreshing auth state...'); if (await _auth.isLoggedIn()) { print('AppState: Token exists, fetching username...'); _username = await _auth.login(); if (_username != null) { print('AppState: Auth refresh successful: $_username'); } else { print('AppState: Auth refresh failed - no username'); } } else { print('AppState: No valid token found'); _username = null; } } catch (e) { print('AppState: Auth refresh error: $e'); _username = null; } notifyListeners(); } // Force a completely fresh login (clears stored tokens) Future forceLogin() async { try { print('AppState: Starting forced fresh login...'); _username = await _auth.forceLogin(); if (_username != null) { print('AppState: Forced login successful: $_username'); } else { print('AppState: Forced login failed - no username returned'); } } catch (e) { print('AppState: Forced login error: $e'); _username = null; } notifyListeners(); } // Validate current token/credentials Future validateToken() async { try { return await _auth.isLoggedIn(); } catch (e) { print('AppState: Token validation error: $e'); return false; } } // ---------- Profiles ---------- List get profiles => List.unmodifiable(_profiles); bool isEnabled(CameraProfile p) => _enabled.contains(p); List get enabledProfiles => _profiles.where(isEnabled).toList(growable: false); void toggleProfile(CameraProfile p, bool e) { if (e) { _enabled.add(p); } else { _enabled.remove(p); // Safety: Always have at least one enabled profile if (_enabled.isEmpty) { final builtIn = _profiles.firstWhere((profile) => profile.builtin, orElse: () => _profiles.first); _enabled.add(builtIn); } } _saveEnabledProfiles(); notifyListeners(); } void addOrUpdateProfile(CameraProfile p) { final idx = _profiles.indexWhere((x) => x.id == p.id); if (idx >= 0) { _profiles[idx] = p; } else { _profiles.add(p); _enabled.add(p); _saveEnabledProfiles(); } ProfileService().save(_profiles); notifyListeners(); } void deleteProfile(CameraProfile p) { if (p.builtin) return; _enabled.remove(p); _profiles.removeWhere((x) => x.id == p.id); _saveEnabledProfiles(); ProfileService().save(_profiles); notifyListeners(); } // Save enabled profile IDs to disk Future _saveEnabledProfiles() async { final prefs = await SharedPreferences.getInstance(); await prefs.setStringList( _enabledPrefsKey, _enabled.map((p) => p.id).toList(), ); } // ---------- Add‑camera session ---------- void startAddSession() { _session = AddCameraSession(profile: enabledProfiles.first); notifyListeners(); } void updateSession({ double? directionDeg, CameraProfile? profile, LatLng? target, }) { if (_session == null) return; bool dirty = false; if (directionDeg != null && directionDeg != _session!.directionDegrees) { _session!.directionDegrees = directionDeg; dirty = true; } if (profile != null && profile != _session!.profile) { _session!.profile = profile; dirty = true; } if (target != null) { _session!.target = target; dirty = true; } if (dirty) notifyListeners(); // <-- slider & map update } void cancelSession() { _session = null; notifyListeners(); } void commitSession() { if (_session?.target == null) return; _queue.add( PendingUpload( coord: _session!.target!, direction: _session!.directionDegrees, profile: _session!.profile, ), ); _saveQueue(); _session = null; // Restart uploader when new items are added _startUploader(); notifyListeners(); } // ---------- Queue persistence ---------- Future _saveQueue() async { final prefs = await SharedPreferences.getInstance(); final jsonList = _queue.map((e) => e.toJson()).toList(); await prefs.setString('queue', jsonEncode(jsonList)); } Future _loadQueue() async { final prefs = await SharedPreferences.getInstance(); final jsonStr = prefs.getString('queue'); if (jsonStr == null) return; final list = jsonDecode(jsonStr) as List; _queue ..clear() ..addAll(list.map((e) => PendingUpload.fromJson(e))); } // ---------- Uploader ---------- void _startUploader() { _uploadTimer?.cancel(); // No uploads without auth or queue. if (_queue.isEmpty) return; _uploadTimer = Timer.periodic(const Duration(seconds: 10), (t) async { if (_queue.isEmpty) return; // Retrieve access after every tick (accounts for re-login) final access = await _auth.getAccessToken(); if (access == null) return; // not logged in final item = _queue.first; bool ok; if (_uploadMode == UploadMode.simulate) { // Simulate successful upload without calling real API print('AppState: UploadMode.simulate - simulating upload for ${item.coord}'); await Future.delayed(const Duration(seconds: 1)); // Simulate network delay ok = true; print('AppState: Simulated upload successful'); } else { // Real upload -- pass uploadMode so uploader can switch between prod and sandbox final up = Uploader(access, () { _queue.remove(item); _saveQueue(); notifyListeners(); }, uploadMode: _uploadMode); ok = await up.upload(item); } if (ok && _uploadMode == UploadMode.simulate) { // Remove manually for simulate mode _queue.remove(item); _saveQueue(); notifyListeners(); } if (!ok) { item.attempts++; if (item.attempts >= 3) { // give up until next launch _uploadTimer?.cancel(); } else { await Future.delayed(const Duration(seconds: 20)); } } }); } // ---------- Exposed getters ---------- int get pendingCount => _queue.length; List get pendingUploads => List.unmodifiable(_queue); // ---------- Queue management ---------- void clearQueue() { print('AppState: Clearing upload queue (${_queue.length} items)'); _queue.clear(); _saveQueue(); notifyListeners(); } void removeFromQueue(PendingUpload upload) { print('AppState: Removing upload from queue: ${upload.coord}'); _queue.remove(upload); _saveQueue(); notifyListeners(); } }