import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../data/models/trip_model.dart'; import '../data/trip_repository.dart'; import '../data/trip_socket_service.dart'; import '../data/trip_event_merger.dart'; import 'trip_event.dart'; import 'trip_state.dart'; /// TripBloc — Bloc الوحيد للراكب (docs/03 §14). /// /// يتعامل مع Lifecycle كامل: إنشاء → تتبّع → إتمام → تقييم. /// المصادر الثلاث: أحداث المستخدم، WebSocket، FCM — تمر عبر TripEventMerger. class TripBloc extends Bloc { final TripRepository _tripRepo; final TripSocketService _socket; final TripEventMerger _merger; Timer? _reconciliationTimer; TripBloc({ required TripRepository tripRepo, required TripSocketService socket, TripEventMerger? merger, }) : _tripRepo = tripRepo, _socket = socket, _merger = merger ?? TripEventMerger(), super(const TripState()) { on(_onCheckActive); on(_onRequested); on(_onUpdateReceived); on(_onDriverLocation); on(_onCancel); on(_onRetry); on(_onRate); on(_onClear); // استمع لتحديثات TripEventMerger. _merger.updateStream.listen((update) { add(TripUpdateReceived( tripId: update.tripId, status: update.status, driverId: update.driverId, extra: update.extra, )); }); _merger.locationStream.listen((loc) { add(TripDriverLocation( tripId: loc.tripId, lat: loc.lat, lng: loc.lng, heading: loc.heading, speed: loc.speed, )); }); // اتصل بالـWebSocket. _socket.connect(); } /// فحص رحلة نشطة عند بدء التشغيل أو العودة من الخلفية. Future _onCheckActive(TripCheckActive event, Emitter emit) async { try { final trip = await _tripRepo.getActiveTrip(); if (trip != null) { emit(state.copyWith( requestStatus: _statusFromTrip(trip.status), trip: trip, )); _joinTripRoom(trip.id); _startReconciliation(trip.id); } } catch (_) { // خطأ صامت — سنحاول مرة أخرى عند إعادة الاتصال. } } /// طلب رحلة جديدة. Future _onRequested(TripRequested event, Emitter emit) async { emit(state.copyWith(requestStatus: TripRequestStatus.loading, clearError: true)); try { final trip = await _tripRepo.createTrip( pickupLat: event.pickupLat, pickupLng: event.pickupLng, pickupAddress: event.pickupAddress, dropoffLat: event.dropoffLat, dropoffLng: event.dropoffLng, dropoffAddress: event.dropoffAddress, vehicleClass: event.vehicleClass, paymentMethod: event.paymentMethod, couponCode: event.couponCode, ); emit(state.copyWith( requestStatus: TripRequestStatus.searching, trip: trip, )); _joinTripRoom(trip.id); _startReconciliation(trip.id); } catch (e) { emit(state.copyWith( requestStatus: TripRequestStatus.error, error: e.toString(), )); } } /// تحديث حالة الرحلة — من WebSocket أو FCM عبر TripEventMerger. void _onUpdateReceived(TripUpdateReceived event, Emitter emit) { final currentTrip = state.trip; if (currentTrip == null || currentTrip.id != event.tripId) return; // التصفية الصامتة: تجاهل الأحداث القديمة. final currentOrder = statusOrder(currentTrip.status); final newOrder = statusOrder(event.status); if (newOrder >= 0 && newOrder <= currentOrder) return; // تحديث النموذج محلياً. final updatedTrip = TripModel( id: currentTrip.id, status: event.status, clientId: currentTrip.clientId, pickupLat: currentTrip.pickupLat, pickupLng: currentTrip.pickupLng, pickupAddress: currentTrip.pickupAddress, dropoffLat: currentTrip.dropoffLat, dropoffLng: currentTrip.dropoffLng, dropoffAddress: currentTrip.dropoffAddress, vehicleClass: currentTrip.vehicleClass, fare: (event.extra['fare'] as num?) ?? currentTrip.fare, currency: event.extra['currency'] as String? ?? currentTrip.currency, paymentMethod: currentTrip.paymentMethod, driverId: event.driverId ?? currentTrip.driverId, createdAt: currentTrip.createdAt, updatedAt: DateTime.now(), ); final newStatus = _statusFromTrip(event.status); // استخراج معلومات السائق من extra. final vehicle = event.extra['vehicle'] as Map?; emit(state.copyWith( requestStatus: newStatus, trip: updatedTrip, driverName: event.extra['driverName'] as String?, vehicleMake: vehicle?['make'] as String?, vehicleModel: vehicle?['model'] as String?, vehiclePlate: vehicle?['plate'] as String?, vehicleColor: vehicle?['color'] as String?, )); // حالة نهائية: أوقف مؤقت الم reconciliation. if (isTerminalStatus(event.status)) { _stopReconciliation(); _socket.leaveTrip(event.tripId); } } /// موقع السائق الحي. void _onDriverLocation(TripDriverLocation event, Emitter emit) { if (state.trip?.id != event.tripId) return; emit(state.copyWith( driverLat: event.lat, driverLng: event.lng, driverHeading: event.heading, )); } /// إلغاء الرحلة. Future _onCancel(TripCancelRequested event, Emitter emit) async { if (state.trip == null) return; try { final trip = await _tripRepo.cancelTrip(state.trip!.id); _stopReconciliation(); _socket.leaveTrip(state.trip!.id); emit(state.copyWith( requestStatus: TripRequestStatus.completed, trip: trip, )); } catch (e) { emit(state.copyWith(error: e.toString())); } } /// إعادة المحاولة بعد خطأ. void _onRetry(TripRetry event, Emitter emit) { emit(state.copyWith(requestStatus: TripRequestStatus.idle, clearError: true)); } /// تقييم الرحلة. Future _onRate(TripRate event, Emitter emit) async { if (state.trip == null) return; try { await _tripRepo.rateTrip(state.trip!.id, stars: event.stars, comment: event.comment); emit(state.copyWith(rated: true)); } catch (e) { emit(state.copyWith(error: e.toString())); } } /// مسح الرحلة من الذاكرة. void _onClear(TripClear event, Emitter emit) { _stopReconciliation(); emit(const TripState()); } // --- مساعدات --- void _joinTripRoom(String tripId) { _socket.joinTrip(tripId); } void _startReconciliation(String tripId) { _stopReconciliation(); _reconciliationTimer = Timer.periodic(const Duration(seconds: 25), (_) async { try { final trip = await _tripRepo.getTrip(tripId); if (trip != null) { add(TripUpdateReceived( tripId: trip.id, status: trip.status, driverId: trip.driverId, extra: { if (trip.fare != null) 'fare': trip.fare, if (trip.currency != null) 'currency': trip.currency, }, )); } } catch (_) { // خطأ صامت — سيحاول مرة أخرى. } }); } void _stopReconciliation() { _reconciliationTimer?.cancel(); _reconciliationTimer = null; } TripRequestStatus _statusFromTrip(String status) { switch (status) { case 'searching': return TripRequestStatus.searching; case 'assigned': case 'driver_arriving': case 'driver_arrived': case 'in_progress': return TripRequestStatus.active; case 'completed': case 'paid': return TripRequestStatus.completed; case 'cancelled': case 'no_drivers': case 'expired': return TripRequestStatus.completed; default: return TripRequestStatus.searching; } } @override Future close() { _stopReconciliation(); _merger.dispose(); _socket.disconnect(); return super.close(); } }