import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'handle_validator.dart'; import 'rider_profile_models.dart'; /// Service managing public rider profile persistence, unique handle reservations, /// and privacy preferences in Cloud Firestore. class RiderProfileService { final FirebaseFirestore? _firestore; RiderProfileService({FirebaseFirestore? firestore}) : _firestore = firestore ?? _resolveFirestore(); static FirebaseFirestore? _resolveFirestore() { try { if (Firebase.apps.isNotEmpty) return FirebaseFirestore.instance; } catch (_) {} return null; } bool get isAvailable => _firestore != null; /// Fetches a rider's profile by UID. Future getProfile(String uid) async { final firestore = _firestore; if (firestore == null) return null; try { final doc = await firestore.collection('users').doc(uid).get(); if (!doc.exists || doc.data() == null) return null; return RiderProfile.fromJson(doc.data()!); } catch (e) { debugPrint('[RiderProfileService] Failed to load profile for $uid: $e'); return null; } } /// Realtime stream of a rider's profile by UID. Stream profileStream(String uid) { final firestore = _firestore; if (firestore == null) return Stream.value(null); return firestore .collection('users') .doc(uid) .snapshots() .map((snapshot) { if (!snapshot.exists || snapshot.data() == null) return null; try { return RiderProfile.fromJson(snapshot.data()!); } catch (e) { debugPrint('[RiderProfileService] Stream parse error for $uid: $e'); return null; } }) .handleError((e) { debugPrint('[RiderProfileService] Stream error for $uid: $e'); return null; }); } /// Checks if a normalized handle is available for reservation. /// If [currentUid] is provided, returning true if the handle is already owned by [currentUid]. Future isHandleAvailable(String handle, {String? currentUid}) async { final validation = HandleValidator.validate(handle); if (!validation.isValid) return false; final firestore = _firestore; if (firestore == null) return true; final normalized = validation.normalizedHandle!; try { final doc = await firestore.collection('handles').doc(normalized).get(); if (!doc.exists) return true; final data = doc.data(); final ownerUid = data?['uid'] as String?; return currentUid != null && ownerUid == currentUid; } catch (e) { debugPrint('[RiderProfileService] Error checking handle $normalized: $e'); return false; } } /// Saves or updates a rider's profile while atomically securing the handle reservation. Future saveProfile({ required String uid, required String handle, required String displayName, String bio = '', String region = '', String? avatarPreset, String? avatarUrl, ProfilePrivacySettings? privacySettings, }) async { final validation = HandleValidator.validate(handle); if (!validation.isValid) { throw ArgumentError(validation.error); } final normalizedHandle = validation.normalizedHandle!; final firestore = _firestore; final now = DateTime.now(); if (firestore == null) { // Offline/local fallback return RiderProfile( uid: uid, handle: normalizedHandle, displayName: displayName.trim(), bio: bio.trim(), region: region.trim(), avatarPreset: avatarPreset, avatarUrl: avatarUrl, privacySettings: privacySettings ?? const ProfilePrivacySettings(), createdAt: now, updatedAt: now, ); } final userRef = firestore.collection('users').doc(uid); final handleRef = firestore.collection('handles').doc(normalizedHandle); // Read current user profile to determine previous handle if changing final currentDoc = await userRef.get(); final existingProfile = currentDoc.exists && currentDoc.data() != null ? RiderProfile.fromJson(currentDoc.data()!) : null; final previousHandle = existingProfile?.handle; final isChangingHandle = previousHandle != null && previousHandle != normalizedHandle; await firestore.runTransaction((transaction) async { // 1. Verify handle availability final handleSnapshot = await transaction.get(handleRef); if (handleSnapshot.exists) { final ownerUid = handleSnapshot.data()?['uid'] as String?; if (ownerUid != uid) { throw StateError('Handle @$normalizedHandle is already taken.'); } } // 2. Reserve new handle transaction.set(handleRef, { 'handle': normalizedHandle, 'uid': uid, 'createdAt': FieldValue.serverTimestamp(), }); // 3. Release previous handle if changed if (isChangingHandle) { final oldHandleRef = firestore.collection('handles').doc(previousHandle); transaction.delete(oldHandleRef); } // 4. Save profile document final updatedProfile = RiderProfile( uid: uid, handle: normalizedHandle, displayName: displayName.trim().isEmpty ? normalizedHandle : displayName.trim(), bio: bio.trim(), region: region.trim(), avatarPreset: avatarPreset, avatarUrl: avatarUrl, privacySettings: privacySettings ?? existingProfile?.privacySettings ?? const ProfilePrivacySettings(), createdAt: existingProfile?.createdAt ?? now, updatedAt: now, ); transaction.set(userRef, updatedProfile.toJson(), SetOptions(merge: true)); }); final saved = await getProfile(uid); return saved ?? RiderProfile( uid: uid, handle: normalizedHandle, displayName: displayName.trim(), bio: bio.trim(), region: region.trim(), avatarPreset: avatarPreset, avatarUrl: avatarUrl, privacySettings: privacySettings ?? const ProfilePrivacySettings(), createdAt: existingProfile?.createdAt ?? now, updatedAt: now, ); } /// Releases the rider's handle and cleans up the profile doc. Future deleteProfile(String uid) async { final firestore = _firestore; if (firestore == null) return; try { final userRef = firestore.collection('users').doc(uid); final doc = await userRef.get(); if (doc.exists && doc.data() != null) { final handle = doc.data()?['username'] as String?; if (handle != null && handle.isNotEmpty) { await firestore.collection('handles').doc(handle.toLowerCase()).delete(); } } await userRef.delete(); } catch (e) { debugPrint('[RiderProfileService] Failed to delete profile for $uid: $e'); rethrow; } } }