import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../services/sensor_fusion_service.dart'; import '../../services/map/map_provider_interface.dart'; import '../../services/map/map_providers.dart'; import '../../services/alerts/road_alert_models.dart'; import '../../services/alerts/road_alert_providers.dart'; import '../../services/alerts/road_alert_service.dart'; import '../../features/rides/ride_state_machine.dart'; import '../../database/database.dart'; import '../../core/app_info.dart'; import '../../services/field_test/field_test_logger.dart'; import '../../core/theme/evolution_theme_config.dart'; import '../../services/osm_oauth_service.dart'; import '../../services/auth/auth_providers.dart'; import 'widgets/evolution_settings_view.dart'; import 'widgets/current_plan_card.dart'; class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @override ConsumerState createState() => _SettingsScreenState(); } class _SettingsScreenState extends ConsumerState { late TextEditingController _tomtomKeyController; late TextEditingController _communityKeyController; late TextEditingController _communityEndpointController; bool _isTestingTomTom = false; bool _isTestingCommunity = false; @override void initState() { super.initState(); _tomtomKeyController = TextEditingController( text: ref.read(tomtomApiKeyProvider), ); _communityKeyController = TextEditingController( text: ref.read(communityApiKeyProvider), ); _communityEndpointController = TextEditingController( text: ref.read(communityEndpointUrlProvider), ); } @override void dispose() { _tomtomKeyController.dispose(); _communityKeyController.dispose(); _communityEndpointController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final themeMode = ref.watch(dashboardThemeModeProvider); if (themeMode == DashboardThemeMode.evolution) { return const EvolutionSettingsView(); } final mapType = ref.watch(mapProviderTypeNotifierProvider); final roadAlertsEnabled = ref.watch(roadAlertsEnabledProvider); final audioAlertsEnabled = ref.watch(audioAlertsEnabledProvider); final alertRadiusMeters = ref.watch(alertRadiusMetersProvider); final enabledCategories = ref.watch(enabledCategoriesProvider); final tomtomStatus = ref.watch(tomtomStatusProvider); final communityStatus = ref.watch(communityStatusProvider); return Scaffold( appBar: AppBar(title: const Text('Settings')), body: ListView( padding: const EdgeInsets.all(16.0), children: [ _buildSectionHeader('Membership & Entitlement'), const SizedBox(height: 12), const CurrentPlanCard(), const SizedBox(height: 24), _buildSectionHeader('Account & Cloud Identity'), const SizedBox(height: 12), Card( clipBehavior: Clip.antiAlias, child: Consumer( builder: (context, ref, _) { final authState = ref.watch(authNotifierProvider); final isAuthed = authState.isAuthenticated; final user = authState.user; return InkWell( onTap: () => context.go('/settings/account'), child: Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ Icon( isAuthed ? Icons.account_circle : Icons.account_circle_outlined, color: Colors.cyanAccent, size: 24, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isAuthed ? (user?.displayLabel ?? 'Rider Account') : 'Sign In / Cloud Account', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( isAuthed ? (user?.email ?? 'Cloud Identity Active • Offline rides protected') : 'Optional cloud identity • Local riding always active', style: const TextStyle( fontSize: 12, color: Colors.white70, ), ), ], ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: isAuthed ? Colors.cyanAccent.withValues(alpha: 0.15) : Colors.white10, borderRadius: BorderRadius.circular(6), border: Border.all( color: isAuthed ? Colors.cyanAccent : Colors.white24, ), ), child: Text( isAuthed ? 'SIGNED IN' : 'LOCAL', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: isAuthed ? Colors.cyanAccent : Colors.white54, ), ), ), const SizedBox(width: 6), const Icon( Icons.chevron_right, color: Colors.white54, size: 20, ), ], ), ), ); }, ), ), const SizedBox(height: 24), _buildSectionHeader('Dashboard UI Theme & Design'), const SizedBox(height: 12), Card( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon( Icons.palette_outlined, color: Colors.cyanAccent, size: 20, ), SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Visual Theme Mode', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), SizedBox(height: 2), Text( 'Choose between Classic Cockpit and the alternate MotorSignal Evolution Stitch theme.', style: TextStyle( fontSize: 12, color: Colors.white70, height: 1.3, ), ), ], ), ), ], ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: SegmentedButton( segments: const [ ButtonSegment( value: DashboardThemeMode.classic, label: Text( 'Classic Cockpit', style: TextStyle(fontSize: 12), ), icon: Icon( Icons.dashboard_customize_outlined, size: 18, ), ), ButtonSegment( value: DashboardThemeMode.evolution, label: Text( 'MotorSignal Evolution', style: TextStyle(fontSize: 12), ), icon: Icon(Icons.speed, size: 18), ), ], selected: {themeMode}, onSelectionChanged: (Set selection) { ref .read(dashboardThemeModeProvider.notifier) .setThemeMode(selection.first); }, ), ), ], ), ), ), const SizedBox(height: 24), _buildSectionHeader('Maps & Navigation Engine'), const SizedBox(height: 12), Card( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.map, color: Colors.cyanAccent, size: 20), SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Active Map Engine', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), SizedBox(height: 2), Text( 'Select map rendering backend for live map, route preview, and replay.', style: TextStyle( fontSize: 12, color: Colors.white70, height: 1.3, ), ), ], ), ), ], ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: SegmentedButton( segments: const [ ButtonSegment( value: AppMapType.openStreetMap, label: Text( 'OpenStreetMap', style: TextStyle(fontSize: 12), ), icon: Icon(Icons.public, size: 18), ), ButtonSegment( value: AppMapType.googleMaps, label: Text( 'Google Maps', style: TextStyle(fontSize: 12), ), icon: Icon(Icons.map_outlined, size: 18), ), ], selected: {mapType}, onSelectionChanged: (Set selection) { ref .read(mapProviderTypeNotifierProvider.notifier) .setMapProviderType(selection.first); }, ), ), const SizedBox(height: 16), const Divider(), const SizedBox(height: 8), Consumer( builder: (context, ref, _) { final osmAuth = ref.watch(osmOAuthServiceProvider); final isAuthed = osmAuth.isAuthenticated; final user = osmAuth.user; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Expanded( child: Text( 'OpenStreetMap Account', overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 13, ), ), ), const SizedBox(width: 8), Chip( label: Text( isAuthed ? 'Connected' : 'Disconnected', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: isAuthed ? Colors.cyanAccent : Colors.grey, ), ), backgroundColor: isAuthed ? Colors.cyanAccent.withValues(alpha: 0.15) : Colors.white10, visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, ), ], ), const SizedBox(height: 6), if (isAuthed && user != null) ...[ ListTile( contentPadding: EdgeInsets.zero, leading: CircleAvatar( backgroundImage: user.avatarUrl != null ? NetworkImage(user.avatarUrl!) : null, child: user.avatarUrl == null ? const Icon(Icons.person) : null, ), title: Text( user.displayName, style: const TextStyle( fontWeight: FontWeight.bold, ), ), subtitle: Text( 'Changesets: ${user.changesetCount} • GPS Trace Sync Enabled', style: const TextStyle(fontSize: 11), ), trailing: TextButton.icon( style: TextButton.styleFrom( foregroundColor: Colors.redAccent, ), icon: const Icon(Icons.link_off, size: 16), label: const Text( 'Disconnect', style: TextStyle(fontSize: 12), ), onPressed: () => ref .read(osmOAuthServiceProvider.notifier) .logout(), ), ), ] else ...[ const Text( 'Sign in to OpenStreetMap via OAuth 2.0 PKCE to upload ride GPX tracks and contribute traces.', style: TextStyle( fontSize: 11, color: Colors.white60, ), ), const SizedBox(height: 10), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Colors.cyanAccent, foregroundColor: Colors.black, minimumSize: const Size(double.infinity, 38), ), icon: const Icon(Icons.login, size: 18), label: const Text( 'Connect OpenStreetMap', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, ), ), onPressed: osmAuth.isLoading ? null : () => ref .read(osmOAuthServiceProvider.notifier) .startLoginFlow(), ), ], ], ); }, ), ], ), ), ), const SizedBox(height: 24), _buildSectionHeader('Road Alerts & Incidents'), const SizedBox(height: 12), Card( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( activeThumbColor: Colors.cyanAccent, contentPadding: const EdgeInsets.symmetric( horizontal: 12.0, vertical: 0.0, ), title: const Text( 'Road Alerts', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), ), subtitle: const Text( 'Display real-time police, hazards, and traffic incidents on map.', style: TextStyle(fontSize: 11), ), value: roadAlertsEnabled, onChanged: (val) { ref .read(roadAlertsEnabledProvider.notifier) .setEnabled(val); }, ), const Divider(), SwitchListTile( activeThumbColor: Colors.cyanAccent, contentPadding: const EdgeInsets.symmetric( horizontal: 12.0, vertical: 0.0, ), title: const Text( 'Audio Warnings (TTS)', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), ), subtitle: const Text( 'Announce approaching hazards and enforcement over helmet headset.', style: TextStyle(fontSize: 11), ), value: audioAlertsEnabled, onChanged: roadAlertsEnabled ? (val) { ref .read(audioAlertsEnabledProvider.notifier) .setEnabled(val); } : null, ), const Divider(), Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Alert Radius', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), ), SizedBox(height: 4), Text( 'Distance to detect upcoming incidents', style: TextStyle( fontSize: 11, color: Colors.white70, ), ), ], ), ), const SizedBox(width: 12), DropdownButton( value: alertRadiusMeters, items: const [ DropdownMenuItem( value: 152.4, child: Text('500 ft'), ), DropdownMenuItem( value: 304.8, child: Text('1,000 ft'), ), DropdownMenuItem( value: 402.3, child: Text('1/4 Mile'), ), DropdownMenuItem( value: 804.7, child: Text('1/2 Mile'), ), DropdownMenuItem( value: 1609.3, child: Text('1 Mile'), ), ], onChanged: roadAlertsEnabled ? (val) { if (val != null) { ref .read( alertRadiusMetersProvider.notifier, ) .setRadius(val); } } : null, ), ], ), ), const Divider(), const Padding( padding: EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0, ), child: Text( 'Alert Categories', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), ), ), ...RoadAlertCategory.values.map((cat) { final isChecked = enabledCategories.contains(cat); return CheckboxListTile( activeColor: Colors.cyanAccent, checkColor: Colors.black, dense: true, visualDensity: VisualDensity.compact, contentPadding: const EdgeInsets.symmetric( horizontal: 12.0, vertical: 0.0, ), title: Text( cat.displayName, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, ), ), secondary: Icon(cat.icon, color: cat.color, size: 20), value: isChecked, onChanged: roadAlertsEnabled ? (val) { ref .read(enabledCategoriesProvider.notifier) .toggleCategory(cat, val ?? true); } : null, ); }), ], ), ), ), const SizedBox(height: 16), // Community Traffic Provider Card Card( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildProviderHeader( icon: Icons.local_police, iconColor: Colors.blueAccent, title: 'Community Traffic Provider', subtitle: 'Supplies Police, Speed Cameras, and Crowd Hazards.', status: communityStatus, ), const SizedBox(height: 16), TextField( controller: _communityKeyController, decoration: const InputDecoration( labelText: 'Community / RapidAPI Key', hintText: 'Enter OpenWebNinja, RapidAPI, or Community API Key', helperText: 'Supplies Police, Speed Cameras, and Crowd Hazards.', helperMaxLines: 2, border: OutlineInputBorder(), isDense: true, ), style: const TextStyle(fontSize: 14), onChanged: (val) { ref.read(communityApiKeyProvider.notifier).setApiKey(val); }, ), const SizedBox(height: 10), TextField( controller: _communityEndpointController, decoration: const InputDecoration( labelText: 'Custom Endpoint URL (Optional)', hintText: 'e.g. https://google-map-places.p.rapidapi.com or custom URL', helperText: 'Leave blank for automatic OpenWebNinja / RapidAPI detection.', helperMaxLines: 2, border: OutlineInputBorder(), isDense: true, ), style: const TextStyle(fontSize: 13), onChanged: (val) { ref .read(communityEndpointUrlProvider.notifier) .setEndpoint(val); }, ), const SizedBox(height: 12), Align( alignment: Alignment.centerRight, child: ElevatedButton.icon( onPressed: _isTestingCommunity ? null : () async { setState(() => _isTestingCommunity = true); final messenger = ScaffoldMessenger.of(context); final provider = ref.read( communityProviderInstance, ); final status = await provider.testConnection( _communityKeyController.text, ); ref.read(communityStatusProvider.notifier).state = status; setState(() => _isTestingCommunity = false); if (mounted) { String msg = ''; if (status == RoadAlertProviderStatus.connected) { msg = 'Community / OpenWebNinja API Connected successfully!'; } else if (status == RoadAlertProviderStatus.authError) { msg = 'Authentication Error: Invalid or unauthorized key.'; } else if (status == RoadAlertProviderStatus.notConfigured) { msg = 'API Key is empty.'; } else if (status == RoadAlertProviderStatus.networkError) { msg = 'Network Error: Check internet connection or endpoint.'; } else if (status == RoadAlertProviderStatus.rateLimited) { msg = 'Rate Limited: Plan limit reached.'; } else { msg = 'Status: ${status.name}'; } messenger.showSnackBar( SnackBar(content: Text(msg)), ); } }, icon: _isTestingCommunity ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.sync), label: const Text('Test Connection'), ), ), ], ), ), ), const SizedBox(height: 16), // TomTom Provider API Key Card Card( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildProviderHeader( icon: Icons.security, iconColor: Colors.cyanAccent, title: 'TomTom Traffic Provider', subtitle: 'Supplies Accidents, Hazards, Closures & Construction data.', status: tomtomStatus, ), const SizedBox(height: 16), TextField( controller: _tomtomKeyController, decoration: const InputDecoration( labelText: 'TomTom API Key', hintText: 'Enter your free TomTom API Key', helperText: 'Supplies Accidents, Hazards, Closures & Construction data.', helperMaxLines: 2, border: OutlineInputBorder(), isDense: true, ), style: const TextStyle(fontSize: 14), onChanged: (val) { ref.read(tomtomApiKeyProvider.notifier).setApiKey(val); }, ), const SizedBox(height: 12), Align( alignment: Alignment.centerRight, child: ElevatedButton.icon( onPressed: _isTestingTomTom ? null : () async { setState(() => _isTestingTomTom = true); final messenger = ScaffoldMessenger.of(context); final provider = ref.read(tomtomProviderInstance); final status = await provider.testConnection( _tomtomKeyController.text, ); ref.read(tomtomStatusProvider.notifier).state = status; setState(() => _isTestingTomTom = false); if (mounted) { String msg = ''; if (status == RoadAlertProviderStatus.connected) { msg = 'TomTom API Connected successfully!'; } else if (status == RoadAlertProviderStatus.authError) { msg = 'Authentication Error: Invalid Key.'; } else if (status == RoadAlertProviderStatus.notConfigured) { msg = 'Key is empty.'; } else if (status == RoadAlertProviderStatus.networkError) { msg = 'Network Error: Failed to connect to API.'; } else if (status == RoadAlertProviderStatus.rateLimited) { msg = 'Rate Limited: Too many requests.'; } else { msg = 'Status: ${status.name}'; } messenger.showSnackBar( SnackBar(content: Text(msg)), ); } }, icon: _isTestingTomTom ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.sync), label: const Text('Test Connection'), ), ), ], ), ), ), // Developer Test Mode Section (Debug Mode Only) if (kDebugMode) ...[ const SizedBox(height: 32), const Text( 'Developer Options', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.orangeAccent, ), ), const SizedBox(height: 16), Card( color: Colors.orange.withValues(alpha: 0.1), shape: RoundedRectangleBorder( side: const BorderSide(color: Colors.orangeAccent), borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon( Icons.bug_report, color: Colors.orangeAccent, size: 20, ), SizedBox(width: 10), Text( 'Road Alert Test Mode', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.orangeAccent, ), ), ], ), const SizedBox(height: 8), const Text( 'Generate simulated test alerts for real-device testing (Debug Build Only).', style: TextStyle(fontSize: 12, color: Colors.white70), ), const SizedBox(height: 16), Wrap( spacing: 8, runSpacing: 8, children: [ ElevatedButton.icon( icon: const Icon( Icons.local_police, color: Colors.blueAccent, size: 18, ), label: const Text('Test Police (100m)'), onPressed: () { final alert = RoadAlert( id: 'sim_police', providerId: 'mock', category: RoadAlertCategory.police, description: 'Simulated police alert', latitude: 0, longitude: 0, distanceFromUser: 100.0, reportedAt: DateTime.now(), lastUpdated: DateTime.now(), source: 'Test', ); ref .read(roadAlertServiceProvider) .simulateAlert(alert); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Simulated Police Alert sent to Dashboard.', ), ), ); }, ), ElevatedButton.icon( icon: const Icon( Icons.warning, color: Colors.orangeAccent, size: 18, ), label: const Text('Test Hazard (250m)'), onPressed: () { final alert = RoadAlert( id: 'sim_hazard', providerId: 'mock', category: RoadAlertCategory.hazard, description: 'Simulated hazard alert', latitude: 0, longitude: 0, distanceFromUser: 250.0, reportedAt: DateTime.now(), lastUpdated: DateTime.now(), source: 'Test', ); ref .read(roadAlertServiceProvider) .simulateAlert(alert); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Simulated Hazard Alert sent to Dashboard.', ), ), ); }, ), ], ), ], ), ), ), ], const SizedBox(height: 24), _buildSectionHeader('Sensors'), const SizedBox(height: 12), Card( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon( Icons.screen_rotation, color: Colors.cyanAccent, size: 20, ), const SizedBox(width: 10), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Calibrate Mount Angle', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), SizedBox(height: 2), Text( 'Place the motorcycle perfectly upright and tap this to zero the lean angle sensor.', style: TextStyle( fontSize: 12, color: Colors.white70, height: 1.3, ), ), ], ), ), ], ), const SizedBox(height: 16), Align( alignment: Alignment.centerRight, child: ElevatedButton( onPressed: () async { final session = ref.read(rideStateMachineProvider); if (session.state == RideState.riding) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Cannot calibrate while a ride is actively recording.', ), ), ); return; } ref .read(sensorFusionServiceProvider) .calibrateUpright(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Sensors Calibrated!'), ), ); } }, child: const Text('Calibrate'), ), ), ], ), ), ), const SizedBox(height: 24), _buildSectionHeader('Data'), const SizedBox(height: 12), Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: () => context.go('/settings/diagnostics'), child: const Padding( padding: EdgeInsets.all(16.0), child: Row( children: [ Icon( Icons.monitor_heart, color: Colors.cyanAccent, size: 20, ), SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Sensor Diagnostics', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), SizedBox(height: 2), Text( 'View raw IMU and GPS values.', style: TextStyle( fontSize: 12, color: Colors.white70, ), ), ], ), ), Icon(Icons.chevron_right, color: Colors.white54, size: 20), ], ), ), ), ), const SizedBox(height: 16), Card( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon( Icons.delete_forever, color: Colors.redAccent, size: 20, ), const SizedBox(width: 10), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Clear Ride History', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), SizedBox(height: 2), Text( 'Permanently delete all recorded rides and performance runs.', style: TextStyle( fontSize: 12, color: Colors.white70, height: 1.3, ), ), ], ), ), ], ), const SizedBox(height: 16), Align( alignment: Alignment.centerRight, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.red.withValues(alpha: 0.2), ), onPressed: () async { final session = ref.read(rideStateMachineProvider); if (session.state == RideState.riding) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Cannot clear history while a ride is actively recording.', ), ), ); return; } final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Clear All Ride History?'), content: const Text( 'This will permanently delete all saved rides, telemetry points, and events. This action cannot be undone.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('CANCEL'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text( 'CLEAR ALL', style: TextStyle( color: Colors.redAccent, fontWeight: FontWeight.bold, ), ), ), ], ), ); if (confirmed == true && mounted) { final db = ref.read(databaseProvider); await db.transaction(() async { await db.delete(db.telemetrySamples).go(); await db.delete(db.rideEvents).go(); await db.delete(db.performanceRuns).go(); await db.delete(db.rides).go(); }); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('All ride history cleared.'), ), ); } } }, child: const Text( 'Clear', style: TextStyle(color: Colors.redAccent), ), ), ), ], ), ), ), const SizedBox(height: 24), _buildFieldTestSection(context, ref), const SizedBox(height: 24), _AppVersionFooter(), ], ), ); } Widget _buildFieldTestSection(BuildContext context, WidgetRef ref) { final isEnabled = ref.watch(fieldTestModeEnabledProvider); final activeSession = ref.watch(activeFieldTestSessionProvider); final logger = ref.watch(fieldTestLoggerProvider); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader('Engineering & Diagnostics'), const SizedBox(height: 12), Card( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Row( children: [ Icon( Icons.biotech, color: isEnabled ? Colors.greenAccent : Colors.white54, size: 24, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Field Test Mode', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, ), ), Text( 'Silent black-box diagnostic recorder', style: const TextStyle( fontSize: 11, color: Colors.white60, ), overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ), const SizedBox(width: 8), Switch( value: isEnabled, activeThumbColor: Colors.greenAccent, onChanged: (val) { ref .read(fieldTestModeEnabledProvider.notifier) .setEnabled(val); }, ), ], ), const Divider(height: 24, color: Colors.white12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Logger Status:', style: TextStyle(color: Colors.white70, fontSize: 13), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: activeSession != null ? Colors.greenAccent.withValues(alpha: 0.15) : (isEnabled ? Colors.cyanAccent.withValues(alpha: 0.15) : Colors.white10), borderRadius: BorderRadius.circular(6), border: Border.all( color: activeSession != null ? Colors.greenAccent : (isEnabled ? Colors.cyanAccent : Colors.white30), ), ), child: Text( activeSession != null ? 'RECORDING (Ride #$activeSession)' : (isEnabled ? 'ARMED (Auto-starts on Ride)' : 'OFF'), style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: activeSession != null ? Colors.greenAccent : (isEnabled ? Colors.cyanAccent : Colors.white54), ), ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Colors.greenAccent.withValues( alpha: 0.2, ), foregroundColor: Colors.greenAccent, side: const BorderSide(color: Colors.greenAccent), ), icon: const Icon(Icons.fact_check, size: 18), label: const Text( 'Pre-Ride Check', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), onPressed: () => context.go('/settings/field-test-pre-ride-check'), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Colors.cyanAccent.withValues( alpha: 0.15, ), foregroundColor: Colors.cyanAccent, side: const BorderSide(color: Colors.cyanAccent), ), icon: const Icon(Icons.assessment, size: 18), label: const Text( 'View Reports', style: TextStyle(fontSize: 12), ), onPressed: () => context.go('/settings/field-test-report'), ), ), const SizedBox(width: 8), Expanded( child: OutlinedButton.icon( icon: const Icon(Icons.receipt_long, size: 18), label: const Text( 'Live Log', style: TextStyle(fontSize: 12), ), onPressed: () => context.go('/settings/field-test-live-log'), ), ), ], ), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: TextButton.icon( style: TextButton.styleFrom( foregroundColor: Colors.redAccent, ), icon: const Icon(Icons.delete_sweep, size: 16), label: const Text( 'Clear Field Test Logs', style: TextStyle(fontSize: 11), ), onPressed: () async { final session = ref.read(rideStateMachineProvider); if (session.state == RideState.riding) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Cannot clear logs while a ride is recording.', ), ), ); return; } final confirm = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Clear All Field Test Logs?'), content: const Text( 'This will delete all stored black-box diagnostic sessions. Standard ride history will NOT be affected.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('CANCEL'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text( 'CLEAR', style: TextStyle(color: Colors.redAccent), ), ), ], ), ); if (confirm == true && context.mounted) { await logger.clearAllSessions(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Field test logs cleared.'), ), ); } } }, ), ), ], ), ), ), ], ); } Widget _buildSectionHeader(String title) { return Text( title, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, letterSpacing: 0.4, color: Colors.cyanAccent, ), ); } Widget _buildProviderHeader({ required IconData icon, required Color iconColor, required String title, required String subtitle, required RoadAlertProviderStatus status, }) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: iconColor, size: 20), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), softWrap: true, ), const SizedBox(height: 2), Text( subtitle, style: const TextStyle( fontSize: 12, color: Colors.white70, height: 1.3, ), softWrap: true, ), const SizedBox(height: 8), Align( alignment: Alignment.centerLeft, child: _buildStatusBadge(status), ), ], ), ), ], ); } Widget _buildStatusBadge(RoadAlertProviderStatus status) { String label; Color color; switch (status) { case RoadAlertProviderStatus.connected: label = 'Connected'; color = Colors.greenAccent; break; case RoadAlertProviderStatus.configured: label = 'Configured'; color = Colors.cyanAccent; break; case RoadAlertProviderStatus.authError: label = 'Auth Error'; color = Colors.redAccent; break; case RoadAlertProviderStatus.rateLimited: label = 'Rate Limited'; color = Colors.orangeAccent; break; case RoadAlertProviderStatus.networkError: label = 'Network Error'; color = Colors.amberAccent; break; case RoadAlertProviderStatus.notConfigured: label = 'Not Set'; color = Colors.white38; break; } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withValues(alpha: 0.15), border: Border.all(color: color), borderRadius: BorderRadius.circular(8), ), child: Text( label, style: TextStyle( color: color, fontSize: 11, fontWeight: FontWeight.bold, ), ), ); } } /// Read-only footer that surfaces the app's build version inside Settings. /// Resolves the version asynchronously via [AppInfo]; shows a placeholder /// while loading and the literal fallback string if the platform call fails. class _AppVersionFooter extends StatefulWidget { const _AppVersionFooter(); @override State<_AppVersionFooter> createState() => _AppVersionFooterState(); } class _AppVersionFooterState extends State<_AppVersionFooter> { late Future _versionFuture; @override void initState() { super.initState(); _versionFuture = AppInfo.versionString(); } void _showIdentityDialog() { final identity = AppInfo.identity; showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF1E293B), title: const Row( children: [ Icon(Icons.fingerprint, color: Colors.cyanAccent), SizedBox(width: 8), Text( 'Software Identity', style: TextStyle(color: Colors.white, fontSize: 16), ), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _diagRow('Application', 'MotorSignal'), _diagRow('Version', identity.semanticVersion), _diagRow('Build Number', '${identity.buildNumber}'), _diagRow('Release Channel', identity.releaseChannel), _diagRow('Telemetry Schema', '${identity.telemetrySchemaVersion}'), _diagRow('Export Schema', '${identity.exportSchemaVersion}'), _diagRow( 'Git Commit', identity.gitCommit.length > 12 ? identity.gitCommit.substring(0, 12) : identity.gitCommit, ), _diagRow('Build Timestamp', identity.buildTimestamp), ], ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text( 'CLOSE', style: TextStyle(color: Colors.cyanAccent), ), ), ], ), ); } Widget _diagRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: const TextStyle(color: Colors.white70, fontSize: 12), ), Text( value, style: const TextStyle( color: Colors.cyanAccent, fontWeight: FontWeight.bold, fontSize: 12, fontFamily: 'monospace', ), ), ], ), ); } @override Widget build(BuildContext context) { return Center( child: InkWell( onTap: _showIdentityDialog, borderRadius: BorderRadius.circular(8), child: FutureBuilder( future: _versionFuture, builder: (context, snapshot) { final text = snapshot.data ?? AppInfo.fallbackVersion; return Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Column( children: [ Text( 'Motorcycle Dash v$text', textAlign: TextAlign.center, style: const TextStyle( color: Colors.white54, fontSize: 12, letterSpacing: 0.4, ), ), const SizedBox(height: 2), const Text( 'Tap for forensic build identity', style: TextStyle(color: Colors.white24, fontSize: 9), ), ], ), ); }, ), ), ); } }