using Flurl.Http; using Newtonsoft.Json; namespace InnovEnergy.App.Backend.Services; public static class WeatherService { public record DailyWeather( string Date, double TempMin, double TempMax, double SunshineHours, double PrecipitationMm, string Description ); private static readonly Dictionary GeoCache = new(); /// /// Returns a 7-day weather forecast for the given city, or null on any failure. /// public static async Task?> GetForecastAsync(string? city, string? country, string? region = null) { if (string.IsNullOrWhiteSpace(city)) return null; try { var coords = await GeocodeAsync(city, region); if (coords == null) return null; var (lat, lon) = coords.Value; return await FetchForecastAsync(lat, lon); } catch (Exception ex) { Console.Error.WriteLine($"[WeatherService] Error fetching forecast for '{city}': {ex.Message}"); return null; } } /// /// Formats a forecast list into a compact text block for AI prompt injection. /// public static string FormatForPrompt(List forecast) { var lines = forecast.Select(d => { var date = DateTime.Parse(d.Date); var dayName = date.ToString("ddd dd MMM"); return $"- {dayName}: {d.TempMin:F0}–{d.TempMax:F0}°C, {d.Description}, {d.SunshineHours:F1}h sunshine, {d.PrecipitationMm:F0}mm rain"; }); return "WEATHER FORECAST (next 7 days):\n" + string.Join("\n", lines); } /// /// Extracts a geocodable city name from a Location field that may contain a full address. /// Handles Swiss formats like "Street 5, 8604 Volketswil" → "Volketswil" /// Also tries the Region field as fallback. /// private static IEnumerable ExtractSearchTerms(string city, string? region) { // If it contains a comma, try the part after the last comma (often "PostalCode City") if (city.Contains(',')) { var afterComma = city.Split(',').Last().Trim(); // Strip leading postal code (digits) → "8604 Volketswil" → "Volketswil" var withoutPostal = System.Text.RegularExpressions.Regex.Replace(afterComma, @"^\d+\s*", "").Trim(); if (!string.IsNullOrEmpty(withoutPostal)) yield return withoutPostal; if (!string.IsNullOrEmpty(afterComma)) yield return afterComma; } // Try the raw value as-is yield return city; // Fallback to Region if (!string.IsNullOrWhiteSpace(region)) yield return region; } private static async Task<(double Lat, double Lon)?> GeocodeAsync(string city, string? region = null) { if (GeoCache.TryGetValue(city, out var cached)) return cached; foreach (var term in ExtractSearchTerms(city, region)) { var url = $"https://geocoding-api.open-meteo.com/v1/search?name={Uri.EscapeDataString(term)}&count=1&language=en"; var json = await url.GetStringAsync(); var data = JsonConvert.DeserializeObject(json); if (data?.results != null && data.results.Count > 0) { var lat = (double)data.results[0].latitude; var lon = (double)data.results[0].longitude; GeoCache[city] = (lat, lon); Console.WriteLine($"[WeatherService] Geocoded '{city}' (matched on '{term}') → ({lat:F4}, {lon:F4})"); return (lat, lon); } } Console.WriteLine($"[WeatherService] Could not geocode location: '{city}'"); return null; } private static async Task?> FetchForecastAsync(double lat, double lon) { var url = $"https://api.open-meteo.com/v1/forecast" + $"?latitude={lat}&longitude={lon}" + "&daily=temperature_2m_max,temperature_2m_min,sunshine_duration,precipitation_sum,weathercode" + "&timezone=Europe/Zurich&forecast_days=7"; var json = await url.GetStringAsync(); var data = JsonConvert.DeserializeObject(json); if (data?.daily == null) return null; var dates = data.daily.time; var tempMax = data.daily.temperature_2m_max; var tempMin = data.daily.temperature_2m_min; var sun = data.daily.sunshine_duration; var precip = data.daily.precipitation_sum; var codes = data.daily.weathercode; var forecast = new List(); for (int i = 0; i < dates.Count; i++) { forecast.Add(new DailyWeather( Date: (string)dates[i], TempMin: (double)tempMin[i], TempMax: (double)tempMax[i], SunshineHours: Math.Round((double)sun[i] / 3600.0, 1), PrecipitationMm: (double)precip[i], Description: WeatherCodeToDescription((int)codes[i]) )); } Console.WriteLine($"[WeatherService] Fetched {forecast.Count}-day forecast."); return forecast; } private static string WeatherCodeToDescription(int code) => code switch { 0 => "Clear sky", 1 => "Mainly clear", 2 => "Partly cloudy", 3 => "Overcast", 45 => "Fog", 48 => "Depositing rime fog", 51 => "Light drizzle", 53 => "Moderate drizzle", 55 => "Dense drizzle", 61 => "Slight rain", 63 => "Moderate rain", 65 => "Heavy rain", 66 => "Light freezing rain", 67 => "Heavy freezing rain", 71 => "Slight snow", 73 => "Moderate snow", 75 => "Heavy snow", 77 => "Snow grains", 80 => "Slight showers", 81 => "Moderate showers", 82 => "Violent showers", 85 => "Slight snow showers", 86 => "Heavy snow showers", 95 => "Thunderstorm", 96 => "Thunderstorm with slight hail", 99 => "Thunderstorm with heavy hail", _ => "Unknown" }; }