import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../services/auth/auth_providers.dart'; import '../../../services/clubs/club_providers.dart'; import '../../../services/clubs/club_service.dart'; /// State of active membership mutations (join, request, withdraw, accept, decline, leave). @immutable class ClubMembershipActionState { final bool isSubmitting; final String? error; final String? success; final String? activeClubId; const ClubMembershipActionState({ this.isSubmitting = false, this.error, this.success, this.activeClubId, }); ClubMembershipActionState copyWith({ bool? isSubmitting, String? error, String? success, String? activeClubId, bool clearError = false, bool clearSuccess = false, }) { return ClubMembershipActionState( isSubmitting: isSubmitting ?? this.isSubmitting, error: clearError ? null : (error ?? this.error), success: clearSuccess ? null : (success ?? this.success), activeClubId: activeClubId ?? this.activeClubId, ); } @override bool operator ==(Object other) => identical(this, other) || other is ClubMembershipActionState && runtimeType == other.runtimeType && isSubmitting == other.isSubmitting && error == other.error && success == other.success && activeClubId == other.activeClubId; @override int get hashCode => isSubmitting.hashCode ^ error.hashCode ^ success.hashCode ^ activeClubId.hashCode; } /// Controller coordinating membership lifecycle operations. /// Interacts with authoritative ClubService and synchronizes Riverpod streams. class ClubMembershipController extends StateNotifier { final Ref _ref; final String clubId; ClubMembershipController(this._ref, this.clubId) : super(ClubMembershipActionState(activeClubId: clubId)); void clearMessages() { state = state.copyWith(clearError: true, clearSuccess: true); } /// Direct self-join for open admission clubs. Future joinOpenClub([String? targetClubId]) async { final effectiveClubId = targetClubId ?? clubId; return _performMutation( clubId: effectiveClubId, mutation: (service, uid) => service.joinOpenClub(effectiveClubId, callerUid: uid), successMessage: 'Successfully joined club!', onSuccessInvalidate: () { _ref.invalidate(currentMemberProvider(effectiveClubId)); _ref.invalidate(clubDetailProvider(effectiveClubId)); _ref.invalidate(myClubsProvider); }, ); } /// Request admission for request-policy clubs. Future requestToJoinClub([String? targetClubId]) async { final effectiveClubId = targetClubId ?? clubId; return _performMutation( clubId: effectiveClubId, mutation: (service, uid) => service.requestToJoinClub(effectiveClubId, callerUid: uid), successMessage: 'Join request submitted to club leadership.', onSuccessInvalidate: () { _ref.invalidate(currentMemberProvider(effectiveClubId)); }, ); } /// Withdraw an existing pending request. Future withdrawJoinRequest([String? targetClubId]) async { final effectiveClubId = targetClubId ?? clubId; return _performMutation( clubId: effectiveClubId, mutation: (service, uid) => service.withdrawJoinRequest(effectiveClubId, callerUid: uid), successMessage: 'Join request withdrawn.', onSuccessInvalidate: () { _ref.invalidate(currentMemberProvider(effectiveClubId)); }, ); } /// Accept an incoming leadership invitation. Future acceptInvitation([String? targetClubId]) async { final effectiveClubId = targetClubId ?? clubId; return _performMutation( clubId: effectiveClubId, mutation: (service, uid) => service.respondToInvitation(effectiveClubId, accept: true, callerUid: uid), successMessage: 'Invitation accepted! Welcome to the club.', onSuccessInvalidate: () { _ref.invalidate(currentMemberProvider(effectiveClubId)); _ref.invalidate(clubDetailProvider(effectiveClubId)); _ref.invalidate(myClubsProvider); _ref.invalidate(myClubInvitesProvider); }, ); } /// Decline an incoming leadership invitation. Future declineInvitation([String? targetClubId]) async { final effectiveClubId = targetClubId ?? clubId; return _performMutation( clubId: effectiveClubId, mutation: (service, uid) => service.respondToInvitation(effectiveClubId, accept: false, callerUid: uid), successMessage: 'Invitation declined.', onSuccessInvalidate: () { _ref.invalidate(currentMemberProvider(effectiveClubId)); _ref.invalidate(myClubInvitesProvider); }, ); } /// Voluntarily leave an active club membership. /// Enforces invariant: Owner cannot leave. Future leaveClub([String? targetClubId]) async { final effectiveClubId = targetClubId ?? clubId; return _performMutation( clubId: effectiveClubId, mutation: (service, uid) => service.leaveClub(effectiveClubId, callerUid: uid), successMessage: 'You have left the club.', onSuccessInvalidate: () { _ref.invalidate(currentMemberProvider(effectiveClubId)); _ref.invalidate(clubDetailProvider(effectiveClubId)); _ref.invalidate(myClubsProvider); }, ); } Future _performMutation({ required String clubId, required Future Function(ClubService service, String uid) mutation, required String successMessage, required VoidCallback onSuccessInvalidate, }) async { // Concurrency guard: reject duplicate or overlapping mutations in-flight if (state.isSubmitting) { return false; } final user = _ref.read(currentUserProvider); if (user == null || user.isAnonymous) { state = state.copyWith( error: 'You must be signed in with a registered account to perform this action.', clearSuccess: true, ); return false; } state = ClubMembershipActionState( isSubmitting: true, activeClubId: clubId, ); try { final service = _ref.read(clubServiceProvider); await mutation(service, user.uid); // Perform minimal, surgical invalidations for this exact lifecycle transition onSuccessInvalidate(); state = ClubMembershipActionState( isSubmitting: false, success: successMessage, activeClubId: clubId, ); return true; } on ClubOperationException catch (e) { String message; switch (e.code) { case 'banned': message = 'Membership is unavailable for this club.'; break; case 'owner-cannot-leave': message = 'Club owner cannot leave without transferring ownership first.'; break; case 'policy-mismatch': message = 'This club does not allow open joining. Please submit a request.'; break; case 'not-found': message = 'Club or membership record was not found.'; break; case 'unavailable': message = 'Club services are temporarily unavailable. Please try again later.'; break; default: message = e.message.isNotEmpty ? e.message : 'Unable to update club membership right now. Please try again.'; break; } state = ClubMembershipActionState( isSubmitting: false, error: message, activeClubId: clubId, ); return false; } catch (e) { state = ClubMembershipActionState( isSubmitting: false, error: 'Unable to update club membership right now. Please try again.', activeClubId: clubId, ); return false; } } } /// Family provider for ClubMembershipController scoped per club. final clubMembershipControllerProvider = StateNotifierProvider.autoDispose .family((ref, clubId) { return ClubMembershipController(ref, clubId); });