import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import '../../core/exceptions/firebase_exception.dart' show FirebaseServiceException; import '../local/local_storage_service.dart'; import '../local/models/item.dart'; import 'models/firebase_config.dart'; /// Service for Firebase integration (optional cloud sync, storage, auth, notifications, analytics). /// /// This service provides: /// - Cloud Firestore for optional metadata sync and backup /// - Firebase Storage for optional media storage /// - Firebase Authentication for user login/logout /// - Firebase Cloud Messaging for push notifications /// - Firebase Analytics for optional analytics /// /// The service is modular and optional - can be enabled/disabled without affecting other modules. /// When disabled, all methods return safely without throwing errors. /// /// The service maintains offline-first behavior by syncing with local storage /// and only using Firebase as an optional cloud backup/sync layer. class FirebaseService { /// Firebase configuration (determines which services are enabled). final FirebaseConfig config; /// Local storage service for offline-first behavior. final LocalStorageService localStorage; /// Firestore instance (null if not enabled). FirebaseFirestore? _firestore; /// Firebase Storage instance (null if not enabled). FirebaseStorage? _storage; /// Firebase Auth instance (null if not enabled). firebase_auth.FirebaseAuth? _auth; /// Firebase Messaging instance (null if not enabled). FirebaseMessaging? _messaging; /// Firebase Analytics instance (null if not enabled). FirebaseAnalytics? _analytics; /// Whether Firebase has been initialized. bool _initialized = false; /// Current user from Firebase Auth (if enabled). firebase_auth.User? _firebaseUser; /// Creates a [FirebaseService] instance. /// /// [config] - Firebase configuration (determines which services are enabled). /// [localStorage] - Local storage service for offline-first behavior. FirebaseService({ required this.config, required this.localStorage, }); /// Gets the current Firebase Auth user (null if not logged in or auth disabled). firebase_auth.User? get currentFirebaseUser => _firebaseUser; /// Checks if Firebase is enabled and initialized. bool get isEnabled => config.enabled && _initialized; /// Checks if a user is logged in via Firebase Auth. bool get isLoggedIn => _auth != null && _firebaseUser != null; /// Initializes Firebase services based on configuration. /// /// Must be called before using any Firebase services. /// If Firebase is disabled, this method does nothing. /// /// Throws [FirebaseServiceException] if initialization fails. Future initialize() async { if (!config.enabled) { return; // Firebase disabled, nothing to initialize } try { // Initialize Firebase Core (required for all services) await Firebase.initializeApp(); // Initialize enabled services if (config.firestoreEnabled) { _firestore = FirebaseFirestore.instance; // Enable offline persistence for Firestore _firestore!.settings = const Settings( persistenceEnabled: true, cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED, ); } if (config.storageEnabled) { _storage = FirebaseStorage.instance; } if (config.authEnabled) { _auth = firebase_auth.FirebaseAuth.instance; // Listen for auth state changes _auth!.authStateChanges().listen((firebase_auth.User? user) { _firebaseUser = user; }); _firebaseUser = _auth!.currentUser; } if (config.messagingEnabled) { _messaging = FirebaseMessaging.instance; // Request notification permissions await _messaging!.requestPermission( alert: true, badge: true, sound: true, ); } if (config.analyticsEnabled) { _analytics = FirebaseAnalytics.instance; } _initialized = true; } catch (e) { throw FirebaseServiceException('Failed to initialize Firebase: $e'); } } /// Logs in a user with email and password. /// /// [email] - User email address. /// [password] - User password. /// /// Returns the Firebase Auth user. /// /// Throws [FirebaseServiceException] if auth is disabled or login fails. Future loginWithEmailPassword({ required String email, required String password, }) async { if (!config.enabled || !config.authEnabled) { throw FirebaseServiceException('Firebase Auth is not enabled'); } if (!_initialized || _auth == null) { throw FirebaseServiceException( 'Firebase not initialized. Call initialize() first.'); } try { final credential = await _auth!.signInWithEmailAndPassword( email: email, password: password, ); _firebaseUser = credential.user; return _firebaseUser!; } catch (e) { throw FirebaseServiceException('Failed to login: $e'); } } /// Logs out the current user. /// /// Throws [FirebaseServiceException] if auth is disabled or logout fails. Future logout() async { if (!config.enabled || !config.authEnabled) { throw FirebaseServiceException('Firebase Auth is not enabled'); } if (!_initialized || _auth == null) { throw FirebaseServiceException( 'Firebase not initialized. Call initialize() first.'); } try { await _auth!.signOut(); _firebaseUser = null; } catch (e) { throw FirebaseServiceException('Failed to logout: $e'); } } /// Syncs local items to Firestore (cloud backup). /// /// [userId] - User ID to associate items with (for multi-user support). /// /// Throws [FirebaseServiceException] if Firestore is disabled or sync fails. Future syncItemsToFirestore(String userId) async { if (!config.enabled || !config.firestoreEnabled) { throw FirebaseServiceException('Firestore is not enabled'); } if (!_initialized || _firestore == null) { throw FirebaseServiceException( 'Firestore not initialized. Call initialize() first.'); } try { // Get all local items final items = await localStorage.getAllItems(); // Batch write to Firestore final batch = _firestore!.batch(); final collection = _firestore!.collection('users').doc(userId).collection('items'); for (final item in items) { final docRef = collection.doc(item.id); batch.set(docRef, { 'id': item.id, 'data': item.data, 'created_at': item.createdAt, 'updated_at': item.updatedAt, }); } await batch.commit(); } catch (e) { throw FirebaseServiceException('Failed to sync items to Firestore: $e'); } } /// Syncs items from Firestore to local storage. /// /// [userId] - User ID to fetch items for. /// /// Throws [FirebaseServiceException] if Firestore is disabled or sync fails. Future syncItemsFromFirestore(String userId) async { if (!config.enabled || !config.firestoreEnabled) { throw FirebaseServiceException('Firestore is not enabled'); } if (!_initialized || _firestore == null) { throw FirebaseServiceException( 'Firestore not initialized. Call initialize() first.'); } try { final snapshot = await _firestore! .collection('users') .doc(userId) .collection('items') .get(); for (final doc in snapshot.docs) { final data = doc.data(); final item = Item( id: data['id'] as String, data: data['data'] as Map, createdAt: data['created_at'] as int, updatedAt: data['updated_at'] as int, ); // Only insert if not already in local storage (avoid duplicates) final existing = await localStorage.getItem(item.id); if (existing == null) { await localStorage.insertItem(item); } } } catch (e) { throw FirebaseServiceException('Failed to sync items from Firestore: $e'); } } /// Uploads a file to Firebase Storage. /// /// [file] - File to upload. /// [path] - Storage path (e.g., 'users/userId/media/image.jpg'). /// /// Returns the download URL. /// /// Throws [FirebaseServiceException] if Storage is disabled or upload fails. Future uploadFile(File file, String path) async { if (!config.enabled || !config.storageEnabled) { throw FirebaseServiceException('Firebase Storage is not enabled'); } if (!_initialized || _storage == null) { throw FirebaseServiceException( 'Firebase Storage not initialized. Call initialize() first.'); } try { final ref = _storage!.ref().child(path); await ref.putFile(file); return await ref.getDownloadURL(); } catch (e) { throw FirebaseServiceException('Failed to upload file: $e'); } } /// Gets the FCM token for push notifications. /// /// Returns the FCM token, or null if messaging is disabled. Future getFcmToken() async { if (!config.enabled || !config.messagingEnabled || _messaging == null) { return null; } try { return await _messaging!.getToken(); } catch (e) { return null; } } /// Logs an event to Firebase Analytics. /// /// [eventName] - Name of the event. /// [parameters] - Optional event parameters. /// /// Does nothing if Analytics is disabled. Future logEvent(String eventName, {Map? parameters}) async { if (!config.enabled || !config.analyticsEnabled || _analytics == null) { return; } try { // Convert Map to Map for Firebase Analytics Map? analyticsParams; if (parameters != null) { analyticsParams = parameters.map((key, value) => MapEntry(key, value as Object)); } await _analytics!.logEvent( name: eventName, parameters: analyticsParams, ); } catch (e) { // Silently fail - analytics failures shouldn't break the app } } /// Disposes of Firebase resources. /// /// Should be called when the service is no longer needed. Future dispose() async { if (_auth != null) { await _auth!.signOut(); } _firebaseUser = null; _initialized = false; } }