import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; /// Weather and road surface telemetry derived from real-time meteorology. @immutable class WeatherReport { final double temperatureC; final double temperatureF; final double precipitationMm; final int weatherCode; final String surfaceCondition; final String gripStatus; final bool isDry; final bool isCold; const WeatherReport({ required this.temperatureC, required this.temperatureF, required this.precipitationMm, required this.weatherCode, required this.surfaceCondition, required this.gripStatus, this.isDry = true, this.isCold = false, }); static const defaultReport = WeatherReport( temperatureC: 21.0, temperatureF: 70.0, precipitationMm: 0.0, weatherCode: 0, surfaceCondition: 'Dry Asphalt', gripStatus: '0% Precip • Grip Optimal', isDry: true, isCold: false, ); } class WeatherService { final http.Client _client; final Map _cache = {}; static const Duration _cacheTtl = Duration(minutes: 10); WeatherService({http.Client? client}) : _client = client ?? http.Client(); /// Fetches real-time weather and surface grip assessment from Open-Meteo. Future fetchWeather(double latitude, double longitude) async { final cacheKey = '${latitude.toStringAsFixed(2)}_${longitude.toStringAsFixed(2)}'; final now = DateTime.now(); final cached = _cache[cacheKey]; if (cached != null && now.difference(cached.$1) < _cacheTtl) { return cached.$2; } try { final uri = Uri.parse( 'https://api.open-meteo.com/v1/forecast?' 'latitude=${latitude.toStringAsFixed(4)}&' 'longitude=${longitude.toStringAsFixed(4)}&' 'current=temperature_2m,relative_humidity_2m,precipitation,weather_code', ); final response = await _client.get(uri).timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { final data = jsonDecode(response.body) as Map; final current = data['current'] as Map?; if (current != null) { final tempC = (current['temperature_2m'] as num?)?.toDouble() ?? 21.0; final precipMm = (current['precipitation'] as num?)?.toDouble() ?? 0.0; final code = (current['weather_code'] as num?)?.toInt() ?? 0; final tempF = (tempC * 9.0 / 5.0) + 32.0; final isCold = tempC < 3.0; final isDry = precipMm <= 0.1; String surfaceCondition; String gripStatus; if (isCold && !isDry) { surfaceCondition = 'Icy / Slush Hazard'; gripStatus = 'Severe Low Grip'; } else if (isCold) { surfaceCondition = 'Cold Asphalt'; gripStatus = 'Tire Warmup Advised'; } else if (precipMm > 2.0) { surfaceCondition = 'Heavy Wet Asphalt'; gripStatus = '${precipMm.toStringAsFixed(1)}mm • Standing Water'; } else if (precipMm > 0.1) { surfaceCondition = 'Damp / Wet Surface'; gripStatus = '${precipMm.toStringAsFixed(1)}mm • Grip Reduced'; } else { surfaceCondition = 'Dry Asphalt'; gripStatus = '0% Precip • Grip Optimal'; } final report = WeatherReport( temperatureC: tempC, temperatureF: tempF, precipitationMm: precipMm, weatherCode: code, surfaceCondition: surfaceCondition, gripStatus: gripStatus, isDry: isDry, isCold: isCold, ); _cache[cacheKey] = (now, report); return report; } } } catch (e) { debugPrint('[WeatherService] Error fetching weather: $e'); } return _cache[cacheKey]?.$2 ?? WeatherReport.defaultReport; } } final weatherServiceProvider = Provider((ref) { return WeatherService(); }); final rideWeatherCoordinatesProvider = StateProvider<({double lat, double lng})?>((ref) => null); final rideWeatherProvider = FutureProvider((ref) async { final coords = ref.watch(rideWeatherCoordinatesProvider); if (coords == null) { return WeatherReport.defaultReport; } final service = ref.watch(weatherServiceProvider); return service.fetchWeather(coords.lat, coords.lng); });