import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import '../models/user_model.dart'; import '../models/recommendation_model.dart'; import '../core/constants/app_constants.dart'; import 'google_signin_wrapper.dart'; class FirebaseService { static final FirebaseService _instance = FirebaseService._internal(); factory FirebaseService() => _instance; // Firebase instances final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseMessaging _messaging = FirebaseMessaging.instance; // Initialize Firebase Service FirebaseService._internal(); // Current user stream Stream get authStateChanges => _auth.authStateChanges(); User? get currentUser => _auth.currentUser; bool get isSignedIn => currentUser != null; // Initialize Firebase static Future initialize() async { try { await Firebase.initializeApp(); debugPrint('Firebase initialized successfully'); } catch (e) { debugPrint('Firebase initialization error: $e'); rethrow; } } // Authentication Methods Future signInWithEmail(String email, String password) async { try { final credential = await _auth.signInWithEmailAndPassword( email: email, password: password, ); return credential; } on FirebaseAuthException catch (e) { debugPrint('Sign in error: ${e.message}'); throw _handleAuthException(e); } } Future registerWithEmail( String email, String password, ) async { try { final credential = await _auth.createUserWithEmailAndPassword( email: email, password: password, ); // Create user document in Firestore if (credential.user != null) { await createUserDocument(credential.user!); } return credential; } on FirebaseAuthException catch (e) { debugPrint('Registration error: ${e.message}'); throw _handleAuthException(e); } } Future signInWithGoogle() async { try { debugPrint('🔄 Starting safe Google Sign-In process...'); // Use the crash-safe wrapper final googleSignInWrapper = GoogleSignInWrapper.instance; // Check if Google Sign-In is available bool isAvailable = await googleSignInWrapper.isAvailable(); if (!isAvailable) { throw Exception('Google Sign-In is not available on this device'); } // Attempt sign-in using the wrapper UserCredential? userCredential = await googleSignInWrapper.signIn(); if (userCredential == null) { debugPrint('👤 User cancelled Google Sign-In'); return null; } return userCredential; } catch (e) { debugPrint('❌ Google Sign-In failed: $e'); throw Exception('Google Sign-In failed: $e'); } } Future signOut() async { try { await Future.wait([ _auth.signOut(), GoogleSignInWrapper.instance.signOut(), ]); } catch (e) { debugPrint('Sign out error: $e'); throw Exception('Sign out failed: $e'); } } Future resetPassword(String email) async { try { await _auth.sendPasswordResetEmail(email: email); } on FirebaseAuthException catch (e) { debugPrint('Password reset error: ${e.message}'); throw _handleAuthException(e); } } /// Check if email exists in Firestore (registered user) Future checkEmailExists(String email) async { try { final querySnapshot = await _firestore .collection(AppConstants.usersCollection) .where('email', isEqualTo: email) .limit(1) .get(); return querySnapshot.docs.isNotEmpty; } catch (e) { debugPrint('Check email exists error: $e'); return false; } } // Email Verification Methods Future createUserForVerification( String email, String tempPassword, ) async { try { final credential = await _auth.createUserWithEmailAndPassword( email: email, password: tempPassword, ); return credential; } on FirebaseAuthException catch (e) { debugPrint('Create user for verification error: ${e.message}'); throw _handleAuthException(e); } } Future sendEmailVerification() async { try { final user = _auth.currentUser; if (user != null && !user.emailVerified) { await user.sendEmailVerification(); debugPrint('📧 Verification email sent to ${user.email}'); } } catch (e) { debugPrint('Send email verification error: $e'); throw Exception('Gagal mengirim email verifikasi: $e'); } } Future checkEmailVerified() async { try { await _auth.currentUser?.reload(); return _auth.currentUser?.emailVerified ?? false; } catch (e) { debugPrint('Check email verified error: $e'); return false; } } Future updatePassword(String newPassword) async { try { final user = _auth.currentUser; if (user != null) { await user.updatePassword(newPassword); debugPrint('🔑 Password updated successfully'); } } catch (e) { debugPrint('Update password error: $e'); throw Exception('Gagal memperbarui password: $e'); } } /// Link email/password to existing account (for Google users) Future linkEmailPassword( String email, String password, ) async { try { final user = _auth.currentUser; if (user == null) { throw Exception('User tidak ditemukan'); } // Create email/password credential final credential = EmailAuthProvider.credential( email: email, password: password, ); // Link the credential to the current user final result = await user.linkWithCredential(credential); debugPrint('✅ Email/password linked successfully to ${user.email}'); return result; } on FirebaseAuthException catch (e) { debugPrint('Link email/password error: ${e.code} - ${e.message}'); if (e.code == 'provider-already-linked') { debugPrint('⚠️ Provider already linked, updating password to match new input...'); try { await _auth.currentUser?.updatePassword(password); return null; } catch (err) { debugPrint('Failed to update password on already linked account: $err'); } } else if (e.code == 'credential-already-in-use') { throw Exception('Email ini sudah digunakan akun lain'); } throw _handleAuthException(e); } catch (e) { debugPrint('Link email/password error: $e'); throw Exception('Gagal menghubungkan email/password: $e'); } } // User Document Methods Future createUserDocument( User user, { String? name, String? phone, }) async { try { final userDoc = _firestore .collection(AppConstants.usersCollection) .doc(user.uid); // Force create/merge user document to ensure it exists // This is crucial for self-healing if data was deleted final userModel = UserModel( id: user.uid, email: user.email!, displayName: name ?? user.displayName, phoneNumber: phone, photoUrl: user.photoURL, preferences: UserPreferences(), createdAt: DateTime.now(), updatedAt: DateTime.now(), isSetupComplete: false, ); // Use SetOptions(merge: true) to safe-write await userDoc.set(userModel.toJson(), SetOptions(merge: true)); debugPrint('✅ User document created/merged for ${user.uid}'); return userModel; } catch (e) { debugPrint('Create user document error: $e'); throw Exception('Failed to create user document: $e'); } } Future isUserRegistered(String uid) async { try { final doc = await _firestore .collection(AppConstants.usersCollection) .doc(uid) .get(); return doc.exists; } catch (e) { return false; } } Future getUserData(String userId) async { try { final doc = await _firestore .collection(AppConstants.usersCollection) .doc(userId) .get(); if (doc.exists && doc.data() != null) { return UserModel.fromJson(doc.data()!); } return null; } catch (e) { debugPrint('Get user data error: $e'); throw Exception('Failed to get user data: $e'); } } /// Get user data by email address Future getUserDataByEmail(String email) async { try { final querySnapshot = await _firestore .collection(AppConstants.usersCollection) .where('email', isEqualTo: email) .limit(1) .get(); if (querySnapshot.docs.isNotEmpty) { return UserModel.fromJson(querySnapshot.docs.first.data()); } return null; } catch (e) { debugPrint('Get user data by email error: $e'); return null; } } Future updateUserData(UserModel user) async { try { await _firestore .collection(AppConstants.usersCollection) .doc(user.id) .update(user.copyWith(updatedAt: DateTime.now()).toJson()); } catch (e) { debugPrint('Update user data error: $e'); throw Exception('Failed to update user data: $e'); } } Future updateUserPreferences( String userId, UserPreferences preferences, ) async { try { final doc = await _firestore .collection(AppConstants.usersCollection) .doc(userId) .get(); if (!doc.exists) throw Exception('User not found'); final currentData = doc.data() as Map; final dailyPreferences = Map.from( currentData['dailyPreferences'] ?? {}); // Sinkronisasi Balik: Profil -> Daily Check-in (keinginan) bool hasFood = preferences.favoriteCategories.any((c) => c.toLowerCase().contains('makanan') || c.toLowerCase().contains('snack') || c.toLowerCase().contains('dessert') || c.toLowerCase().contains('pastry') || c.toLowerCase().contains('sarapan') || c.toLowerCase().contains('berkuah')); bool hasDrink = preferences.favoriteCategories.any((c) => c.toLowerCase().contains('minuman')); if (hasFood && hasDrink) { dailyPreferences['keinginan'] = 'keduanya'; } else if (hasDrink && !hasFood) { dailyPreferences['keinginan'] = 'minuman'; } else if (hasFood && !hasDrink) { dailyPreferences['keinginan'] = 'makanan'; } // Sinkronkan juga budget dari profil ke daily checkin hari ini dailyPreferences['budget'] = preferences.budgetRange; final data = preferences.toJson(); // 🔍 DEBUG — hapus setelah debugging selesai debugPrint('=== SAVE PREFERENCES DEBUG ==='); debugPrint('userId: $userId'); debugPrint('favoriteCategories: ${preferences.favoriteCategories}'); debugPrint('new keinginan: ${dailyPreferences['keinginan']}'); debugPrint('=============================='); await _firestore .collection(AppConstants.usersCollection) .doc(userId) .update({ 'preferences': data, 'dailyPreferences': dailyPreferences, 'updatedAt': DateTime.now().toIso8601String(), }); debugPrint('✅ Preferences saved successfully (and synced to dailyPreferences)'); } catch (e) { debugPrint('Update user preferences error: $e'); throw Exception('Failed to update user preferences: $e'); } } Future setSetupComplete(String userId) async { try { await _firestore .collection(AppConstants.usersCollection) .doc(userId) .update({ 'isSetupComplete': true, 'updatedAt': DateTime.now().toIso8601String(), }); } catch (e) { debugPrint('Set setup complete error: $e'); throw Exception('Failed to set setup complete: $e'); } } // Recommendations Methods Future saveRecommendation(RecommendationModel recommendation) async { try { await _firestore .collection(AppConstants.recommendationsCollection) .doc(recommendation.id) .set(recommendation.toJson()); } catch (e) { debugPrint('Save recommendation error: $e'); throw Exception('Failed to save recommendation: $e'); } } Future> getUserRecommendations( String userId, { int limit = 20, }) async { try { // Simplified query to avoid index issues - just get by userId first final query = await _firestore .collection(AppConstants.recommendationsCollection) .where('userId', isEqualTo: userId) .limit(limit) .get(); // Sort in memory to avoid composite index requirement final recommendations = query.docs .map((doc) => RecommendationModel.fromJson(doc.data())) .toList(); // Sort by createdAt in descending order recommendations.sort((a, b) => b.createdAt.compareTo(a.createdAt)); return recommendations; } catch (e) { debugPrint('Get user recommendations error: $e'); // Return empty list instead of throwing exception for better UX return []; } } Future toggleRecommendationFavorite( String recommendationId, bool isFavorite, ) async { try { await _firestore .collection(AppConstants.recommendationsCollection) .doc(recommendationId) .update({'isFavorite': isFavorite}); } catch (e) { debugPrint('Toggle recommendation favorite error: $e'); throw Exception('Failed to update recommendation: $e'); } } Future deleteRecommendation(String recommendationId) async { try { await _firestore .collection(AppConstants.recommendationsCollection) .doc(recommendationId) .delete(); } catch (e) { debugPrint('Delete recommendation error: $e'); throw Exception('Failed to delete recommendation: $e'); } } // Push Notifications Future initializeNotifications() async { try { // Request permission NotificationSettings settings = await _messaging.requestPermission( alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true, ); if (settings.authorizationStatus == AuthorizationStatus.authorized) { debugPrint('User granted permission for notifications'); // Get FCM token String? token = await _messaging.getToken(); if (token != null && currentUser != null) { await _saveFCMToken(currentUser!.uid, token); } // Listen for token refresh _messaging.onTokenRefresh.listen((token) { if (currentUser != null) { _saveFCMToken(currentUser!.uid, token); } }); } } catch (e) { debugPrint('Initialize notifications error: $e'); } } Future _saveFCMToken(String userId, String token) async { try { await _firestore .collection(AppConstants.usersCollection) .doc(userId) .update({ 'fcmToken': token, 'updatedAt': DateTime.now().toIso8601String(), }); } catch (e) { debugPrint('Save FCM token error: $e'); } } // Error handling String _handleAuthException(FirebaseAuthException e) { switch (e.code) { case 'invalid-credential': return 'Email atau Password salah'; case 'user-not-found': return 'Pengguna tidak ditemukan'; case 'wrong-password': return 'Password salah'; case 'email-already-in-use': return 'Email sudah digunakan'; case 'weak-password': return 'Password terlalu lemah'; case 'invalid-email': return 'Format email tidak valid'; case 'user-disabled': return 'Akun telah dinonaktifkan'; case 'too-many-requests': return 'Terlalu banyak percobaan, coba lagi nanti'; case 'operation-not-allowed': return 'Operasi tidak diizinkan'; default: return e.message ?? 'Terjadi kesalahan'; } } }