import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'rider_profile_models.dart'; /// Manages mutual friendships, friend requests, blocking, and social boundaries. class FriendshipService { final FirebaseFirestore? _firestore; FriendshipService({FirebaseFirestore? firestore}) : _firestore = firestore ?? _resolveFirestore(); static FirebaseFirestore? _resolveFirestore() { try { if (Firebase.apps.isNotEmpty) return FirebaseFirestore.instance; } catch (_) {} return null; } bool get isAvailable => _firestore != null; /// Determines relationship status between [myUid] and [otherUid]. Future getRelationshipStatus( String myUid, String otherUid, ) async { if (myUid == otherUid) return FriendshipStatus.self; final firestore = _firestore; if (firestore == null) return FriendshipStatus.none; try { final userRef = firestore.collection('users').doc(myUid); final otherRef = firestore.collection('users').doc(otherUid); // Check if user has blocked other or other has blocked user final myBlockDoc = await userRef.collection('blocks').doc(otherUid).get(); if (myBlockDoc.exists) return FriendshipStatus.blocked; final otherBlockDoc = await otherRef.collection('blocks').doc(myUid).get(); if (otherBlockDoc.exists) return FriendshipStatus.blocked; // Check mutual friend final friendDoc = await userRef.collection('friends').doc(otherUid).get(); if (friendDoc.exists) return FriendshipStatus.friend; // Check incoming request final incomingDoc = await userRef .collection('friend_requests_incoming') .doc(otherUid) .get(); if (incomingDoc.exists) return FriendshipStatus.requestIncoming; // Check outgoing request final outgoingDoc = await userRef .collection('friend_requests_outgoing') .doc(otherUid) .get(); if (outgoingDoc.exists) return FriendshipStatus.requestOutgoing; return FriendshipStatus.none; } catch (e) { debugPrint('[FriendshipService] Error checking relationship status: $e'); return FriendshipStatus.none; } } /// Sends a friend request from [sender] to [receiverUid]. Future sendFriendRequest({ required RiderProfile sender, required String receiverUid, }) async { if (sender.uid == receiverUid) { throw ArgumentError('Cannot send a friend request to yourself.'); } final firestore = _firestore; if (firestore == null) { throw StateError('Social service is offline.'); } final receiverRef = firestore.collection('users').doc(receiverUid); final receiverDoc = await receiverRef.get(); if (!receiverDoc.exists || receiverDoc.data() == null) { throw StateError('Rider not found.'); } final receiverProfile = RiderProfile.fromJson(receiverDoc.data()!); if (!receiverProfile.privacySettings.allowFriendRequests) { throw StateError('This rider is not accepting friend requests.'); } // Check existing relationship final status = await getRelationshipStatus(sender.uid, receiverUid); if (status == FriendshipStatus.friend) { throw StateError('You are already friends with this rider.'); } if (status == FriendshipStatus.requestOutgoing) { throw StateError('Friend request already sent.'); } if (status == FriendshipStatus.requestIncoming) { throw StateError('This rider already sent you a friend request.'); } if (status == FriendshipStatus.blocked) { throw StateError('Unable to send request to this rider.'); } final requestId = '${sender.uid}_$receiverUid'; final now = DateTime.now(); final request = FriendRequest( requestId: requestId, senderUid: sender.uid, senderHandle: sender.handle, senderDisplayName: sender.displayName, senderAvatarPreset: sender.avatarPreset, senderAvatarUrl: sender.avatarUrl, receiverUid: receiverUid, sentAt: now, ); final batch = firestore.batch(); // 1. Add to receiver's incoming requests final incomingRef = receiverRef .collection('friend_requests_incoming') .doc(sender.uid); batch.set(incomingRef, request.toJson()); // 2. Add to sender's outgoing requests final outgoingRef = firestore .collection('users') .doc(sender.uid) .collection('friend_requests_outgoing') .doc(receiverUid); batch.set(outgoingRef, request.toJson()); await batch.commit(); } /// Accepts an incoming friend request and establishes mutual friendship. Future acceptFriendRequest({ required RiderProfile me, required FriendRequest request, }) async { final firestore = _firestore; if (firestore == null) { throw StateError('Social service is offline.'); } final senderUid = request.senderUid; final now = DateTime.now(); final batch = firestore.batch(); // 1. Remove from receiver's incoming requests final myIncomingRef = firestore .collection('users') .doc(me.uid) .collection('friend_requests_incoming') .doc(senderUid); batch.delete(myIncomingRef); // 2. Remove from sender's outgoing requests final senderOutgoingRef = firestore .collection('users') .doc(senderUid) .collection('friend_requests_outgoing') .doc(me.uid); batch.delete(senderOutgoingRef); // 3. Add to receiver's friends final myFriendRef = firestore .collection('users') .doc(me.uid) .collection('friends') .doc(senderUid); final myFriendSummary = FriendSummary( uid: senderUid, handle: request.senderHandle, displayName: request.senderDisplayName, avatarPreset: request.senderAvatarPreset, avatarUrl: request.senderAvatarUrl, friendedAt: now, ); batch.set(myFriendRef, myFriendSummary.toJson()); // 4. Add to sender's friends final senderFriendRef = firestore .collection('users') .doc(senderUid) .collection('friends') .doc(me.uid); final senderFriendSummary = FriendSummary( uid: me.uid, handle: me.handle, displayName: me.displayName, avatarPreset: me.avatarPreset, avatarUrl: me.avatarUrl, friendedAt: now, ); batch.set(senderFriendRef, senderFriendSummary.toJson()); await batch.commit(); } /// Declines an incoming friend request. Future declineFriendRequest({ required String myUid, required String senderUid, }) async { final firestore = _firestore; if (firestore == null) return; final batch = firestore.batch(); final myIncomingRef = firestore .collection('users') .doc(myUid) .collection('friend_requests_incoming') .doc(senderUid); batch.delete(myIncomingRef); final senderOutgoingRef = firestore .collection('users') .doc(senderUid) .collection('friend_requests_outgoing') .doc(myUid); batch.delete(senderOutgoingRef); await batch.commit(); } /// Cancels an outgoing friend request. Future cancelFriendRequest({ required String myUid, required String receiverUid, }) async { final firestore = _firestore; if (firestore == null) return; final batch = firestore.batch(); final myOutgoingRef = firestore .collection('users') .doc(myUid) .collection('friend_requests_outgoing') .doc(receiverUid); batch.delete(myOutgoingRef); final receiverIncomingRef = firestore .collection('users') .doc(receiverUid) .collection('friend_requests_incoming') .doc(myUid); batch.delete(receiverIncomingRef); await batch.commit(); } /// Removes a mutual friend. Future removeFriend({ required String myUid, required String friendUid, }) async { final firestore = _firestore; if (firestore == null) return; final batch = firestore.batch(); final myFriendRef = firestore .collection('users') .doc(myUid) .collection('friends') .doc(friendUid); batch.delete(myFriendRef); final theirFriendRef = firestore .collection('users') .doc(friendUid) .collection('friends') .doc(myUid); batch.delete(theirFriendRef); await batch.commit(); } /// Blocks another rider, removing any friendship or pending requests. Future blockRider({ required String myUid, required String targetUid, String? targetHandle, String? targetDisplayName, }) async { if (myUid == targetUid) return; final firestore = _firestore; if (firestore == null) return; final batch = firestore.batch(); // Remove any mutual friendship batch.delete( firestore .collection('users') .doc(myUid) .collection('friends') .doc(targetUid), ); batch.delete( firestore .collection('users') .doc(targetUid) .collection('friends') .doc(myUid), ); // Remove any pending requests batch.delete( firestore .collection('users') .doc(myUid) .collection('friend_requests_incoming') .doc(targetUid), ); batch.delete( firestore .collection('users') .doc(myUid) .collection('friend_requests_outgoing') .doc(targetUid), ); batch.delete( firestore .collection('users') .doc(targetUid) .collection('friend_requests_incoming') .doc(myUid), ); batch.delete( firestore .collection('users') .doc(targetUid) .collection('friend_requests_outgoing') .doc(myUid), ); // Add block record final block = BlockedRider( blockedUid: targetUid, blockedHandle: targetHandle, blockedDisplayName: targetDisplayName, blockedAt: DateTime.now(), ); final blockRef = firestore .collection('users') .doc(myUid) .collection('blocks') .doc(targetUid); batch.set(blockRef, block.toJson()); await batch.commit(); // Clean up active shared group rides try { await _cleanupSharedGroupRidesOnBlock( firestore: firestore, myUid: myUid, targetUid: targetUid, ); } catch (e) { debugPrint('[FriendshipService] Error cleaning up shared group rides on block: $e'); } } static Future _cleanupSharedGroupRidesOnBlock({ required FirebaseFirestore firestore, required String myUid, required String targetUid, }) async { // 1. Rides organized by myUid where targetUid has access final myRidesSnap = await firestore .collection('group_rides') .where('organizerUid', isEqualTo: myUid) .where('accessUids', arrayContains: targetUid) .get(); for (final rideDoc in myRidesSnap.docs) { try { final pRef = rideDoc.reference.collection('participants').doc(targetUid); final pSnap = await pRef.get(); final wasAccepted = pSnap.exists && pSnap.data()?['status'] == 'accepted'; final batch = firestore.batch(); final rideUpdates = { 'accessUids': FieldValue.arrayRemove([targetUid]), 'updatedAt': DateTime.now().toIso8601String(), }; if (wasAccepted) { rideUpdates['participantCount'] = FieldValue.increment(-1); } batch.update(rideDoc.reference, rideUpdates); if (pSnap.exists) { batch.update(pRef, { 'status': 'removed', 'updatedAt': DateTime.now().toIso8601String(), }); } await batch.commit(); } catch (e) { debugPrint('[FriendshipService] Evicting blocked user from ride ${rideDoc.id} failed: $e'); } } // 2. Rides organized by targetUid where myUid has access final theirRidesSnap = await firestore .collection('group_rides') .where('organizerUid', isEqualTo: targetUid) .where('accessUids', arrayContains: myUid) .get(); for (final rideDoc in theirRidesSnap.docs) { try { final pRef = rideDoc.reference.collection('participants').doc(myUid); final pSnap = await pRef.get(); final wasAccepted = pSnap.exists && pSnap.data()?['status'] == 'accepted'; final batch = firestore.batch(); final rideUpdates = { 'accessUids': FieldValue.arrayRemove([myUid]), 'updatedAt': DateTime.now().toIso8601String(), }; if (wasAccepted) { rideUpdates['participantCount'] = FieldValue.increment(-1); } batch.update(rideDoc.reference, rideUpdates); if (pSnap.exists) { batch.update(pRef, { 'status': 'left', 'updatedAt': DateTime.now().toIso8601String(), }); } await batch.commit(); } catch (e) { debugPrint('[FriendshipService] Leaving ride ${rideDoc.id} of blocked user failed: $e'); } } } /// Unblocks a previously blocked rider. Future unblockRider({ required String myUid, required String targetUid, }) async { final firestore = _firestore; if (firestore == null) return; await firestore .collection('users') .doc(myUid) .collection('blocks') .doc(targetUid) .delete(); } /// Stream of current friends. Stream> friendsStream(String uid) { final firestore = _firestore; if (firestore == null) return Stream.value([]); return firestore .collection('users') .doc(uid) .collection('friends') .snapshots() .map((snapshot) { return snapshot.docs .map((doc) => FriendSummary.fromJson(doc.data())) .toList() ..sort((a, b) => a.displayName.compareTo(b.displayName)); }) .handleError((e) { debugPrint('[FriendshipService] Error in friends stream: $e'); return []; }); } /// Stream of incoming friend requests. Stream> incomingRequestsStream(String uid) { final firestore = _firestore; if (firestore == null) return Stream.value([]); return firestore .collection('users') .doc(uid) .collection('friend_requests_incoming') .snapshots() .map((snapshot) { return snapshot.docs .map((doc) => FriendRequest.fromJson(doc.data())) .toList() ..sort((a, b) => b.sentAt.compareTo(a.sentAt)); }) .handleError((e) { debugPrint('[FriendshipService] Error in incoming requests stream: $e'); return []; }); } /// Stream of outgoing friend requests. Stream> outgoingRequestsStream(String uid) { final firestore = _firestore; if (firestore == null) return Stream.value([]); return firestore .collection('users') .doc(uid) .collection('friend_requests_outgoing') .snapshots() .map((snapshot) { return snapshot.docs .map((doc) => FriendRequest.fromJson(doc.data())) .toList() ..sort((a, b) => b.sentAt.compareTo(a.sentAt)); }) .handleError((e) { debugPrint('[FriendshipService] Error in outgoing requests stream: $e'); return []; }); } /// Stream of blocked riders. Stream> blockedRidersStream(String uid) { final firestore = _firestore; if (firestore == null) return Stream.value([]); return firestore .collection('users') .doc(uid) .collection('blocks') .snapshots() .map((snapshot) { return snapshot.docs .map((doc) => BlockedRider.fromJson(doc.data())) .toList() ..sort((a, b) => b.blockedAt.compareTo(a.blockedAt)); }) .handleError((e) { debugPrint('[FriendshipService] Error in blocked riders stream: $e'); return []; }); } }