import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_core/firebase_core.dart'; import 'club_models.dart'; import 'club_wall_models.dart'; /// Manages Club Wall discussion board posts, pinned announcements, /// fist-bump kudos, and rider comments. class ClubWallService { final FirebaseFirestore? _firestore; // In-memory cache of posts by clubId for responsive local state & fallback final Map> _localPosts = {}; final Map>> _controllers = {}; ClubWallService({FirebaseFirestore? firestore}) : _firestore = firestore ?? _resolveFirestore(); static FirebaseFirestore? _resolveFirestore() { try { if (Firebase.apps.isNotEmpty) return FirebaseFirestore.instance; } catch (_) {} return null; } StreamController> _getController(String clubId) { return _controllers.putIfAbsent( clubId, () => StreamController>.broadcast(), ); } List _getSeedPosts(String clubId) { final now = DateTime.now(); return [ ClubWallPost( id: '${clubId}_seed_1', clubId: clubId, authorUid: 'seed_pres', authorDisplayName: 'Marcus Vance', authorHandle: '@marcus_v', authorDisplayRank: 'President', authorRole: ClubRole.owner, content: '🏍️ SATURDAY RIDE BRIEFING: Staging at Ridgepoint Shell at 08:30 AM. Route is Blood Mountain Loop (78 miles, standard pace). Ensure tire pressures and chain tension are checked. Road Captain will lead paceline.', createdAt: now.subtract(const Duration(hours: 3)), isPinned: true, fistBumpCount: 14, fistBumpUserIds: const ['seed_user_2', 'seed_user_3'], comments: [ ClubWallComment( id: 'c1', postId: '${clubId}_seed_1', authorUid: 'seed_rc', authorDisplayName: 'Dave Miller', authorHandle: '@dave_m', authorDisplayRank: 'Road Captain', authorRole: ClubRole.admin, content: 'GPX route cue sheet uploaded to the club rides tab. We will fuel up at mile 42.', createdAt: now.subtract(const Duration(hours: 2)), ), ], ), ClubWallPost( id: '${clubId}_seed_2', clubId: clubId, authorUid: 'seed_tg', authorDisplayName: 'Samir Chen', authorHandle: '@samir_c', authorDisplayRank: 'Tail Gunner', authorRole: ClubRole.member, content: 'Sweep radar active on channel 4. Let\'s maintain 2-second stagger spacing through the canyon twisties. See everyone at staging!', createdAt: now.subtract(const Duration(hours: 5)), isPinned: false, fistBumpCount: 9, fistBumpUserIds: const ['seed_user_1'], ), ClubWallPost( id: '${clubId}_seed_3', clubId: clubId, authorUid: 'seed_mem', authorDisplayName: 'Sarah Jenkins', authorHandle: '@sjenkins', authorDisplayRank: 'Road Marshal', authorRole: ClubRole.member, content: 'Just fitted new Pirelli Diablo Rosso IVs, ready for the elevation climbs this weekend! 💨 Weather radar looks clear.', createdAt: now.subtract(const Duration(hours: 8)), isPinned: false, fistBumpCount: 6, ), ]; } /// Real-time stream of wall posts for a club. Stream> clubWallStream(String clubId) { final controller = _getController(clubId); // Initialize seed posts if not present if (!_localPosts.containsKey(clubId)) { _localPosts[clubId] = _getSeedPosts(clubId); Timer.run(() { if (!controller.isClosed) { controller.add(_sortPosts(_localPosts[clubId]!)); } }); } else { Timer.run(() { if (!controller.isClosed) { controller.add(_sortPosts(_localPosts[clubId]!)); } }); } // Attempt to listen to Firestore if available final firestore = _firestore; if (firestore != null) { firestore .collection('clubs') .doc(clubId) .collection('wall_posts') .orderBy('createdAt', descending: true) .limit(50) .snapshots() .listen( (snap) { if (snap.docs.isNotEmpty) { final firestorePosts = snap.docs.map((d) => ClubWallPost.fromFirestore(d)).toList(); _localPosts[clubId] = firestorePosts; if (!controller.isClosed) { controller.add(_sortPosts(firestorePosts)); } } }, onError: (_) { // Graceful fallback to local cache }, ); } return controller.stream; } List _sortPosts(List posts) { final copy = List.from(posts); copy.sort((a, b) { // Pinned posts always first if (a.isPinned && !b.isPinned) return -1; if (!a.isPinned && b.isPinned) return 1; return b.createdAt.compareTo(a.createdAt); }); return copy; } /// Creates a new post on the Club Wall. Future createPost({ required String clubId, required String authorUid, required String authorDisplayName, String? authorHandle, String? authorAvatarUrl, String? authorDisplayRank, ClubRole authorRole = ClubRole.member, required String content, bool isPinned = false, }) async { final cleanContent = content.trim(); if (cleanContent.isEmpty) { throw ArgumentError('Post content cannot be empty.'); } final post = ClubWallPost( id: 'post_${DateTime.now().millisecondsSinceEpoch}_$authorUid', clubId: clubId, authorUid: authorUid, authorDisplayName: authorDisplayName, authorHandle: authorHandle, authorAvatarUrl: authorAvatarUrl, authorDisplayRank: authorDisplayRank, authorRole: authorRole, content: cleanContent, createdAt: DateTime.now(), isPinned: isPinned, fistBumpCount: 0, fistBumpUserIds: const [], comments: const [], ); // Update local cache & notify stream final posts = _localPosts.putIfAbsent(clubId, () => _getSeedPosts(clubId)); posts.insert(0, post); _localPosts[clubId] = posts; _getController(clubId).add(_sortPosts(posts)); // Try persist to Firestore final firestore = _firestore; if (firestore != null) { try { await firestore .collection('clubs') .doc(clubId) .collection('wall_posts') .doc(post.id) .set(post.toFirestore()); } catch (_) { // Silently preserve in local cache } } return post; } /// Toggles a Fist Bump (kudos) on a post. Future toggleFistBump({ required String clubId, required String postId, required String userUid, }) async { final posts = _localPosts[clubId] ?? []; final index = posts.indexWhere((p) => p.id == postId); if (index == -1) return; final target = posts[index]; final alreadyBumped = target.fistBumpUserIds.contains(userUid); final updatedList = List.from(target.fistBumpUserIds); if (alreadyBumped) { updatedList.remove(userUid); } else { updatedList.add(userUid); } final updatedPost = target.copyWith( fistBumpCount: updatedList.length, fistBumpUserIds: updatedList, ); posts[index] = updatedPost; _localPosts[clubId] = posts; _getController(clubId).add(_sortPosts(posts)); // Firestore sync if available final firestore = _firestore; if (firestore != null) { try { await firestore .collection('clubs') .doc(clubId) .collection('wall_posts') .doc(postId) .update({ 'fistBumpCount': updatedList.length, 'fistBumpUserIds': updatedList, }); } catch (_) {} } } /// Adds a comment to a Club Wall post. Future addComment({ required String clubId, required String postId, required String authorUid, required String authorDisplayName, String? authorHandle, String? authorAvatarUrl, String? authorDisplayRank, ClubRole authorRole = ClubRole.member, required String content, }) async { final cleanContent = content.trim(); if (cleanContent.isEmpty) return; final posts = _localPosts[clubId] ?? []; final index = posts.indexWhere((p) => p.id == postId); if (index == -1) return; final comment = ClubWallComment( id: 'comment_${DateTime.now().millisecondsSinceEpoch}', postId: postId, authorUid: authorUid, authorDisplayName: authorDisplayName, authorHandle: authorHandle, authorAvatarUrl: authorAvatarUrl, authorDisplayRank: authorDisplayRank, authorRole: authorRole, content: cleanContent, createdAt: DateTime.now(), ); final target = posts[index]; final updatedComments = List.from(target.comments)..add(comment); final updatedPost = target.copyWith(comments: updatedComments); posts[index] = updatedPost; _localPosts[clubId] = posts; _getController(clubId).add(_sortPosts(posts)); // Firestore sync final firestore = _firestore; if (firestore != null) { try { await firestore .collection('clubs') .doc(clubId) .collection('wall_posts') .doc(postId) .update({ 'comments': updatedComments.map((c) => c.toJson()).toList(), }); } catch (_) {} } } /// Deletes a post from the Club Wall. Future deletePost({ required String clubId, required String postId, }) async { final posts = _localPosts[clubId] ?? []; posts.removeWhere((p) => p.id == postId); _localPosts[clubId] = posts; _getController(clubId).add(_sortPosts(posts)); final firestore = _firestore; if (firestore != null) { try { await firestore .collection('clubs') .doc(clubId) .collection('wall_posts') .doc(postId) .delete(); } catch (_) {} } } }