import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../auth/auth_providers.dart'; import '../entitlements/entitlement_providers.dart'; import '../map/map_provider_interface.dart'; import 'navigation_handoff.dart'; import 'route_models.dart'; import 'route_provider.dart'; import 'route_service.dart'; /// Provider for RouteService. final routeServiceProvider = Provider((ref) { return RouteService(); }); /// Active RouteProvider implementation (defaults to honest straight-line preview). final activeRouteCalculationProvider = Provider((ref) { return const StraightLineRouteProvider(); }); /// Provider for NavigationHandoffService. final navigationHandoffServiceProvider = Provider((ref) { return const NavigationHandoffService(); }); /// Realtime stream provider for a single route by ID. final routeByIdProvider = StreamProvider.family((ref, routeId) { final service = ref.watch(routeServiceProvider); return service.routeStream(routeId); }); /// Realtime stream provider for routes owned by a user. final userRoutesProvider = StreamProvider.family, String>((ref, uid) { final service = ref.watch(routeServiceProvider); return service.userRoutesStream(uid); }); /// Alias for routeByIdProvider. final routeStreamProvider = routeByIdProvider; /// Realtime stream of routes created by current user. final myRoutesStreamProvider = StreamProvider>((ref) { final user = ref.watch(currentUserProvider); if (user == null || user.uid.isEmpty) { return Stream.value(const []); } final service = ref.watch(routeServiceProvider); return service.userRoutesStream(user.uid); }); /// State of an active route planning session. class RoutePlannerState { final String? routeId; final String title; final String description; final RouteVisibility visibility; final List stops; final double straightLineDistanceMeters; final bool isSaving; final String? errorMessage; final bool isDirty; const RoutePlannerState({ this.routeId, this.title = '', this.description = '', this.visibility = RouteVisibility.private, this.stops = const [], this.straightLineDistanceMeters = 0.0, this.isSaving = false, this.errorMessage, this.isDirty = false, }); double get distanceMiles => straightLineDistanceMeters * 0.000621371; String get formattedDistance => '${distanceMiles.toStringAsFixed(1)} mi (Straight-line)'; String get formattedDistanceMiles => '${distanceMiles.toStringAsFixed(1)} mi'; String get formattedDistanceKm => '${(straightLineDistanceMeters / 1000).toStringAsFixed(1)} km'; List get previewPoints => stops.map((s) => s.latLng).toList(); RoutePlannerState copyWith({ String? routeId, String? title, String? description, RouteVisibility? visibility, List? stops, double? straightLineDistanceMeters, bool? isSaving, String? errorMessage, bool? isDirty, }) { return RoutePlannerState( routeId: routeId ?? this.routeId, title: title ?? this.title, description: description ?? this.description, visibility: visibility ?? this.visibility, stops: stops ?? this.stops, straightLineDistanceMeters: straightLineDistanceMeters ?? this.straightLineDistanceMeters, isSaving: isSaving ?? this.isSaving, errorMessage: errorMessage, isDirty: isDirty ?? this.isDirty, ); } } /// Controller managing in-memory route editing state. class RoutePlannerNotifier extends StateNotifier { final Ref ref; RoutePlannerNotifier(this.ref, [MotorSignalRoute? initialRoute]) : super( initialRoute != null ? RoutePlannerState( routeId: initialRoute.id, title: initialRoute.title, description: initialRoute.description, visibility: initialRoute.visibility, stops: initialRoute.orderedStops, straightLineDistanceMeters: initialRoute.straightLineDistanceMeters, ) : const RoutePlannerState(), ); void loadRoute(MotorSignalRoute route) { state = RoutePlannerState( routeId: route.id, title: route.title, description: route.description, visibility: route.visibility, stops: route.orderedStops, straightLineDistanceMeters: route.straightLineDistanceMeters, isDirty: false, ); } void reset() { state = const RoutePlannerState(); } void setTitle(String title) { state = state.copyWith(title: title, isDirty: true); } void setDescription(String desc) { state = state.copyWith(description: desc, isDirty: true); } void setVisibility(RouteVisibility visibility) { state = state.copyWith(visibility: visibility, isDirty: true); } void addStop({ RouteStop? stop, String? displayName, double? latitude, double? longitude, RouteStopType? stopType, String? notes, }) { final updatedStops = List.from(state.stops); final effectiveOrder = updatedStops.length; final newStop = stop != null ? stop.copyWith(order: effectiveOrder, orderIndex: effectiveOrder) : RouteStop( id: 'stop_${DateTime.now().millisecondsSinceEpoch}_$effectiveOrder', order: effectiveOrder, orderIndex: effectiveOrder, type: stopType ?? RouteStopType.waypoint, stopType: stopType ?? RouteStopType.waypoint, displayName: displayName ?? 'Stop $effectiveOrder', latitude: latitude ?? 0.0, longitude: longitude ?? 0.0, notes: notes ?? '', ); updatedStops.add(newStop); final distance = MotorSignalRoute.calculateTotalDistanceMeters(updatedStops); state = state.copyWith( stops: updatedStops, straightLineDistanceMeters: distance, isDirty: true, errorMessage: null, ); } void updateStop(dynamic target, [RouteStop? maybeStop]) { final updatedStops = List.from(state.stops); if (target is RouteStop) { final idx = updatedStops.indexWhere((s) => s.id == target.id); if (idx == -1) return; updatedStops[idx] = target.copyWith(order: idx, orderIndex: idx); } else if (target is int && maybeStop != null) { if (target < 0 || target >= updatedStops.length) return; updatedStops[target] = maybeStop.copyWith(order: target, orderIndex: target); } else { return; } final distance = MotorSignalRoute.calculateTotalDistanceMeters(updatedStops); state = state.copyWith( stops: updatedStops, straightLineDistanceMeters: distance, isDirty: true, errorMessage: null, ); } void removeStop(dynamic target) { final updatedStops = List.from(state.stops); if (target is int) { if (target < 0 || target >= updatedStops.length) return; updatedStops.removeAt(target); } else if (target is String) { updatedStops.removeWhere((s) => s.id == target); } else { return; } // Re-index remaining stops final reindexed = []; for (int i = 0; i < updatedStops.length; i++) { reindexed.add(updatedStops[i].copyWith(order: i, orderIndex: i)); } final distance = MotorSignalRoute.calculateTotalDistanceMeters(reindexed); state = state.copyWith( stops: reindexed, straightLineDistanceMeters: distance, isDirty: true, errorMessage: null, ); } void reorderStops(int oldIndex, int newIndex) { if (oldIndex < 0 || oldIndex >= state.stops.length) return; if (newIndex < 0 || newIndex > state.stops.length) return; final updatedStops = List.from(state.stops); if (newIndex > oldIndex) newIndex -= 1; final item = updatedStops.removeAt(oldIndex); updatedStops.insert(newIndex, item); final reindexed = []; for (int i = 0; i < updatedStops.length; i++) { reindexed.add(updatedStops[i].copyWith(order: i, orderIndex: i)); } final distance = MotorSignalRoute.calculateTotalDistanceMeters(reindexed); state = state.copyWith( stops: reindexed, straightLineDistanceMeters: distance, isDirty: true, errorMessage: null, ); } void reverseStops() { if (state.stops.length < 2) return; final reversed = state.stops.reversed.toList(); final reindexed = []; for (int i = 0; i < reversed.length; i++) { reindexed.add(reversed[i].copyWith(order: i, orderIndex: i)); } final distance = MotorSignalRoute.calculateTotalDistanceMeters(reindexed); state = state.copyWith( stops: reindexed, straightLineDistanceMeters: distance, isDirty: true, errorMessage: null, ); } Future saveRoute({ String? title, String? description, String? ownerUid, RouteVisibility? visibility, String? attachedGroupRideId, }) async { final effectiveTitle = (title ?? state.title).trim(); if (effectiveTitle.length < 3) { throw ArgumentError('Route title must be at least 3 characters.'); } if (state.stops.isEmpty) { throw ArgumentError('Please add at least one stop to the route.'); } final user = ref.read(currentUserProvider); final effectiveUid = ownerUid ?? user?.uid ?? ''; if (effectiveUid.isEmpty) { throw StateError('Sign-in required to save routes.'); } final isPro = ref.read(isProSubscriberProvider); final service = ref.read(routeServiceProvider); state = state.copyWith(isSaving: true, errorMessage: null); try { final saved = await service.saveRoute( id: state.routeId, title: effectiveTitle, description: (description ?? state.description).trim(), ownerUid: effectiveUid, ownerHandle: user?.email?.split('@').first ?? 'rider', visibility: visibility ?? state.visibility, stops: state.stops, isPro: isPro, attachedGroupRideId: attachedGroupRideId, ); state = state.copyWith( routeId: saved.id, isSaving: false, isDirty: false, ); return saved; } catch (e) { state = state.copyWith( isSaving: false, errorMessage: 'Failed to save route: $e', ); rethrow; } } } /// Global notifier provider for route planner state. final routePlannerNotifierProvider = StateNotifierProvider((ref) { return RoutePlannerNotifier(ref); }); /// Backward-compatible family alias. final routePlannerProvider = StateNotifierProvider.autoDispose .family( (ref, initialRoute) => RoutePlannerNotifier(ref, initialRoute), );