import 'dart:async'; import 'dart:convert'; import 'package:get/get.dart'; import 'package:socket_io_client/socket_io_client.dart' as io_client; import 'package:intaleq_maps/intaleq_maps.dart'; import '../../../constant/box_name.dart'; import '../../../constant/links.dart'; import '../../../env/env.dart'; import '../../../main.dart'; // contains global 'box' + 'storage' import '../../../print.dart'; import '../../functions/encrypt_decrypt.dart'; // r() import 'ride_lifecycle_controller.dart'; import 'nearby_drivers_controller.dart'; import 'map_engine_controller.dart'; class MapSocketController extends GetxController { late io_client.Socket socket; bool isSocketConnected = false; bool _isSocketInitialized = false; // يغطّي نافذة الـ await على قراءة الـ JWT: بلا هذا يقدر نداءان متزامنان // (initializeDataAfterLogin + startSearchingForDriver) يفتحان سوكيتين. bool _isConnecting = false; /// جاهز لاستقبال الأحداث — أي منضمّ فعلياً لغرفة passenger_{id} على السيرفر. bool get isReadyForEvents => isSocketConnected && _isSocketInitialized; Timer? _heartbeatTimer; DateTime? _lastSocketLocationTime; int _socketLocationUpdatesCount = 0; Timer? _watchdogTimer; // 🚌 مواصلاتي — الخط الحالي المشترَك فيه + مستمع تحديث موقع الباص int? _subscribedTransitRouteId; void Function(Map data)? onBusLocationUpdate; void Function(String errorCode)? onTransitError; DateTime? get lastDriverLocationTime => _lastSocketLocationTime; int get socketLocationUpdatesCount => _socketLocationUpdatesCount; /// يقرأ الـ JWT بنفس آلية CRUD._getJwt (SecureStorage أولاً ثم GetStorage). /// passenger_socket.php يرفض أي اتصال بلا jwt، فبدونه لا تصل أي أحداث للراكب. Future _getJwtForSocket() async { try { final String? encryptedJwt = await storage.read(key: BoxName.jwt); if (encryptedJwt != null && encryptedJwt.isNotEmpty) { return r(encryptedJwt).toString().split(Env.addd)[0]; } } catch (e) { Log.print('Error reading JWT from SecureStorage for socket: $e'); } final String? fallback = box.read(BoxName.jwt); if (fallback != null && fallback.toString().isNotEmpty) { try { return r(fallback).toString().split(Env.addd)[0]; } catch (e) { Log.print('Error decrypting fallback JWT for socket: $e'); } } return ''; } /// يجب أن يُنادى **قبل** إنشاء الرحلة: غرف Socket.IO لا تخزّن الأحداث، فإن /// قبل كابتن قبل أن ينضم الراكب لغرفته فإن ride_status_change يُرمى نهائياً /// ولا يبقى إلا الـ FCM/polling. النداء آمن ومتكرر (idempotent). Future initConnectionWithSocket() async { if (isSocketConnected || _isSocketInitialized || _isConnecting) return; _isConnecting = true; String passengerId = box.read(BoxName.passengerID).toString(); final String jwt = await _getJwtForSocket(); if (jwt.isEmpty) { _isConnecting = false; Log.print( "⚠️ No JWT available — passenger socket would be rejected by the server. Aborting connect."); return; } if (passengerId.isEmpty || passengerId == 'null') { _isConnecting = false; Log.print("⚠️ No passengerID yet — deferring socket connect."); return; } Log.print("🔌 Initializing Socket for Passenger: $passengerId"); socket = io_client.io( AppLink.serverSocket, io_client.OptionBuilder() .setTransports(['websocket']) .disableAutoConnect() .setQuery({'id': passengerId, 'jwt': jwt}) .setReconnectionAttempts(20) .setReconnectionDelay(2000) .setReconnectionDelayMax(10000) .enableReconnection() .setTimeout(20000) .setExtraHeaders({'Connection': 'Upgrade'}) .build(), ); _isSocketInitialized = true; _isConnecting = false; socket.connect(); socket.onConnect((_) { Log.print("✅ Socket Connected Successfully"); isSocketConnected = true; _startHeartbeat(); final rideLifecycle = Get.find(); if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) { socket.emit('subscribe_driver_location', { 'ride_id': rideLifecycle.rideId, 'driver_id': rideLifecycle.driverId, }); Log.print("📡 Re-subscribed to driver location after connect"); } if (_subscribedTransitRouteId != null) { socket.emit('subscribe_transit_route', {'route_id': _subscribedTransitRouteId}); Log.print("🚌 Re-subscribed to transit route after connect"); } update(); }); socket.onDisconnect((_) { Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it"); isSocketConnected = false; final rideLifecycle = Get.find(); if (rideLifecycle.isActiveRideState()) { Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect..."); rideLifecycle.startMasterTimerWithInterval(4); } update(); }); socket.onReconnect((_) { Log.print("🔁 Socket Reconnected Successfully!"); isSocketConnected = true; _startHeartbeat(); final rideLifecycle = Get.find(); if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) { socket.emit('subscribe_driver_location', { 'ride_id': rideLifecycle.rideId, 'driver_id': rideLifecycle.driverId, }); Log.print("📡 Re-subscribed to driver location after reconnect"); } if (_subscribedTransitRouteId != null) { socket.emit('subscribe_transit_route', {'route_id': _subscribedTransitRouteId}); Log.print("🚌 Re-subscribed to transit route after reconnect"); } if (rideLifecycle.isActiveRideState()) { Log.print("✅ Socket back online — stopping Fast Polling Fallback"); rideLifecycle.cancelMasterTimer(); } update(); }); socket.onReconnectAttempt((attemptNumber) { Log.print("🔄 Socket Reconnect Attempt #$attemptNumber..."); }); socket.onError((error) { Log.print("❌ Socket Error: $error"); isSocketConnected = false; }); socket.on('connect_error', (error) { Log.print("❌ Socket Connect Error: $error"); isSocketConnected = false; // في الإصدار 1.0.2 أحياناً auto-reconnect لا يعمل بعد connect_error // نتأكد يدوياً من إعادة الاتصال Future.delayed(const Duration(seconds: 3), () { if (!isSocketConnected && _isSocketInitialized) { Log.print("🔄 Manual reconnect after connect_error..."); try { socket.connect(); } catch (e) { Log.print("Manual reconnect error: $e"); } } }); }); socket.on('ride_status_change', (data) { Log.print("📩 Socket Event: ride_status_change -> $data"); _handleRideStatusChangeWithSocket(data); }); socket.on('driver_location_update', (data) { handleDriverLocationUpdate(data); }); // 🚌 مواصلاتي — بثّ موقع الباص الحي لخط مشترَك فيه socket.on('bus_location_update', (data) { if (data == null) return; try { final map = Map.from(data as Map); onBusLocationUpdate?.call(map); } catch (e) { Log.print('Error parsing bus_location_update: $e'); } }); socket.on('transit_error', (data) { if (data == null) return; try { final map = Map.from(data as Map); final code = map['code']?.toString() ?? ''; Log.print('⚠️ transit_error: $code'); onTransitError?.call(code); } catch (e) { Log.print('Error parsing transit_error: $e'); } }); } /// يوصل السوكيت وينتظر انضمامه فعلياً لغرفة الراكب، بسقف زمني. /// يُنادى قبل إنشاء الرحلة حتى لا يقبل كابتن والراكب ما زال خارج غرفته /// (الحدث يُفقد نهائياً في تلك الحالة — لا طابور في غرف Socket.IO). /// لا يُفشل إنشاء الرحلة أبداً: عند انتهاء المهلة نكمل ونتّكل على /// الـ FCM/polling كشبكة احتياطية. Future ensureConnectedBeforeRide({ Duration timeout = const Duration(seconds: 4), }) async { if (isReadyForEvents) return true; await initConnectionWithSocket(); final deadline = DateTime.now().add(timeout); while (!isSocketConnected && DateTime.now().isBefore(deadline)) { await Future.delayed(const Duration(milliseconds: 100)); } if (isSocketConnected) { Log.print("✅ Socket ready before ride creation."); return true; } Log.print( "⚠️ Socket not ready within ${timeout.inSeconds}s — continuing; FCM/polling will cover."); return false; } // ── مواصلاتي: اشتراك/إلغاء اشتراك ببثّ موقع خط ───────────── // يُستدعى عند فتح/إغلاق شاشة تتبع الباص الحي. يضمن السوكيت متصلاً أولاً. Future subscribeToTransitRoute(int routeId) async { _subscribedTransitRouteId = routeId; if (!isSocketConnected) { await initConnectionWithSocket(); // سيُعاد الاشتراك تلقائياً من onConnect إن أضفنا ذلك، لكن نحاول فوراً أيضاً } // socket هو late — لو انسحب initConnectionWithSocket (بلا JWT مثلاً) يبقى // غير مُهيّأ، فقراءته مباشرة ترمي LateInitializationError. if (_isSocketInitialized && socket.connected) { socket.emit('subscribe_transit_route', {'route_id': routeId}); Log.print('🚌 Subscribed to transit route #$routeId'); } } void unsubscribeFromTransitRoute(int routeId) { if (_subscribedTransitRouteId == routeId) _subscribedTransitRouteId = null; if (_isSocketInitialized && isSocketConnected && socket.connected) { socket.emit('unsubscribe_transit_route', {'route_id': routeId}); Log.print('🚌 Unsubscribed from transit route #$routeId'); } } void _startHeartbeat() { _heartbeatTimer?.cancel(); _heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (timer) { if (isSocketConnected && socket.connected) { socket.emit('heartbeat', {'passenger_id': box.read(BoxName.passengerID).toString()}); } }); } bool isSocketHealthy() { if (!isSocketConnected) return false; if (_lastSocketLocationTime == null) return false; final diff = DateTime.now().difference(_lastSocketLocationTime!).inSeconds; return diff < 20; } void _handleRideStatusChangeWithSocket(dynamic data) { if (data == null || data['status'] == null) return; String newStatus = data['status'].toString().toLowerCase(); Log.print("🔔 Socket Status Update: $newStatus"); final rideLifecycle = Get.find(); Map? driverInfo; if (data['driver_info'] != null && data['driver_info'] is Map) { driverInfo = Map.from(data['driver_info']); } switch (newStatus) { case 'accepted': case 'apply': case 'applied': rideLifecycle.processRideAcceptance( driverData: driverInfo, source: "Socket"); break; case 'arrived': rideLifecycle.processDriverArrival("Socket"); break; case 'started': case 'begin': rideLifecycle.processRideBegin(source: "Socket"); break; case 'finished': case 'ended': _onRideFinishedWithSocket(data); break; // cancel_ride_by_driver.php يبعث 'cancelled_by_driver' حرفياً، // و cancel_ride_by_passenger.php يبعث 'cancelled_by_passenger'. // كانت الحالتان تسقطان من الـ switch فيبقى الراكب معلّقاً على الشاشة. case 'cancelled': case 'cancelled_by_driver': case 'canceled_by_driver': rideLifecycle.processRideCancelledByDriver(data, source: "Socket"); break; case 'no_drivers_found': rideLifecycle.showNoDriverDialog(); break; } } void _onRideFinishedWithSocket(dynamic data) { Log.print("🏁 Ride Finished (Socket)"); final rideLifecycle = Get.find(); var rawList = data['DriverList']; List listToSend = []; if (rawList != null) { if (rawList is List) { listToSend = rawList; } else if (rawList is String) { try { listToSend = jsonDecode(rawList); } catch (e) { Log.print("Error decoding DriverList: $e"); } } } if (listToSend.isEmpty && data['price'] != null) { listToSend = [ rideLifecycle.driverId, rideLifecycle.rideId, rideLifecycle.driverToken, data['price'].toString() ]; } rideLifecycle.processRideFinished(listToSend, source: "Socket"); } void handleDriverLocationUpdate(dynamic data) { if (!isSocketConnected || data == null) return; _lastSocketLocationTime = DateTime.now(); _socketLocationUpdatesCount++; final rideLifecycle = Get.find(); if (rideLifecycle.driverId.isEmpty && (data['driver_id'] ?? data['driverId']) != null) { rideLifecycle.driverId = (data['driver_id'] ?? data['driverId']).toString(); } if (_socketLocationUpdatesCount >= 3 && rideLifecycle.locationPollingTimer != null) { Log.print("✅ Socket delivering locations reliably. Stopping polling."); rideLifecycle.stopDriverLocationPolling(); } try { double lat = double.tryParse( (data['latitude'] ?? data['lat'])?.toString() ?? '0') ?? 0; double lng = double.tryParse( (data['longitude'] ?? data['lng'])?.toString() ?? '0') ?? 0; double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0; if (lat == 0 || lng == 0) return; LatLng newPos = LatLng(lat, lng); final nearbyDrivers = Get.find(); if (nearbyDrivers.driverCarsLocationToPassengerAfterApplied.isEmpty) { nearbyDrivers.driverCarsLocationToPassengerAfterApplied.add(newPos); } else { nearbyDrivers.driverCarsLocationToPassengerAfterApplied[0] = newPos; } double speed = double.tryParse(data['speed']?.toString() ?? '0') ?? 0; rideLifecycle.checkAndRecalculateIfDeviated( newPos, heading: heading, speed: speed, ); final mapEngine = Get.find(); if (mapEngine.mapController != null) { double zoom = 16.5; if (speed > 0) { zoom = 17.0 - ((speed - 10) / 70) * 2.5; zoom = zoom.clamp(14.5, 17.0); } mapEngine.mapController! .animateCamera(CameraUpdate.newLatLngZoom(newPos, zoom)); } final dynamic distanceValue = data['distance_m'] ?? data['distance_meters']; final double? distanceMeters = double.tryParse(distanceValue?.toString() ?? ''); final int? etaSeconds = data['eta_seconds'] == null ? null : int.tryParse(data['eta_seconds'].toString()); final bool hasServerMetrics = (etaSeconds != null && etaSeconds > 0) || (distanceMeters != null && distanceMeters > 0); if (hasServerMetrics) { rideLifecycle.updateDriverRouteMetrics( etaSeconds: etaSeconds != null && etaSeconds > 0 ? etaSeconds : null, distanceMeters: distanceMeters, ); } rideLifecycle.updateDriverMarker(newPos, heading); rideLifecycle.updateRemainingRoute(newPos, updateEta: !hasServerMetrics); rideLifecycle.update(); } catch (e) { Log.print('Error in handleDriverLocationUpdate: $e'); } } void disposeRideSocket() { _heartbeatTimer?.cancel(); _watchdogTimer?.cancel(); if (_isSocketInitialized) { socket.disconnect(); socket.dispose(); isSocketConnected = false; _isSocketInitialized = false; Log.print("🔌 Socket Disposed"); } } @override void onClose() { disposeRideSocket(); super.onClose(); } }