package services import ( "encoding/json" "fmt" "io" "net/http" "os" "time" "fotbal-club/internal/models" "gorm.io/gorm" ) type WeatherService struct { db *gorm.DB apiKey string baseURL string client *http.Client } type WeatherResponse struct { Location struct { Name string `json:"name"` Region string `json:"region"` Country string `json:"country"` Lat float64 `json:"lat"` Lon float64 `json:"lon"` TzID string `json:"tz_id"` LocalTime string `json:"localtime"` } `json:"location"` Current struct { LastUpdated string `json:"last_updated"` TempC float64 `json:"temp_c"` TempF float64 `json:"temp_f"` IsDay int `json:"is_day"` Condition struct { Text string `json:"text"` Icon string `json:"icon"` Code int `json:"code"` } `json:"condition"` WindMph float64 `json:"wind_mph"` WindKph float64 `json:"wind_kph"` WindDegree int `json:"wind_degree"` WindDir string `json:"wind_dir"` PressureMb float64 `json:"pressure_mb"` PressureIn float64 `json:"pressure_in"` PrecipMm float64 `json:"precip_mm"` PrecipIn float64 `json:"precip_in"` Humidity int `json:"humidity"` Cloud int `json:"cloud"` FeelsLikeC float64 `json:"feelslike_c"` FeelsLikeF float64 `json:"feelslike_f"` VisKm float64 `json:"vis_km"` VisMiles float64 `json:"vis_miles"` UV float64 `json:"uv"` GustMph float64 `json:"gust_mph"` GustKph float64 `json:"gust_kph"` } `json:"current"` Forecast struct { ForecastDay []struct { Date string `json:"date"` DateEpoch int64 `json:"date_epoch"` Day struct { MaxTempC float64 `json:"maxtemp_c"` MaxTempF float64 `json:"maxtemp_f"` MinTempC float64 `json:"mintemp_c"` MinTempF float64 `json:"mintemp_f"` AvgTempC float64 `json:"avgtemp_c"` AvgTempF float64 `json:"avgtemp_f"` MaxWindMph float64 `json:"maxwind_mph"` MaxWindKph float64 `json:"maxwind_kph"` TotalPrecipMm float64 `json:"totalprecip_mm"` TotalPrecipIn float64 `json:"totalprecip_in"` TotalSnowCm float64 `json:"totalsnow_cm"` AvgVisKm float64 `json:"avgvis_km"` AvgVisMiles float64 `json:"avgvis_miles"` AvgHumidity float64 `json:"avghumidity"` DailyWillItRain int `json:"daily_will_it_rain"` DailyChanceOfRain int `json:"daily_chance_of_rain"` DailyWillItSnow int `json:"daily_will_it_snow"` DailyChanceOfSnow int `json:"daily_chance_of_snow"` Condition struct { Text string `json:"text"` Icon string `json:"icon"` Code int `json:"code"` } `json:"condition"` UV float64 `json:"uv"` } `json:"day"` Astro struct { Sunrise string `json:"sunrise"` Sunset string `json:"sunset"` Moonrise string `json:"moonrise"` Moonset string `json:"moonset"` MoonPhase string `json:"moon_phase"` MoonIllumination float64 `json:"moon_illumination"` IsMoonUp int `json:"is_moon_up"` IsSunUp int `json:"is_sun_up"` } `json:"astro"` Hour []struct { TimeEpoch int64 `json:"time_epoch"` Time string `json:"time"` TempC float64 `json:"temp_c"` TempF float64 `json:"temp_f"` IsDay int `json:"is_day"` Condition struct { Text string `json:"text"` Icon string `json:"icon"` Code int `json:"code"` } `json:"condition"` WindMph float64 `json:"wind_mph"` WindKph float64 `json:"wind_kph"` WindDegree int `json:"wind_degree"` WindDir string `json:"wind_dir"` PressureMb float64 `json:"pressure_mb"` PressureIn float64 `json:"pressure_in"` PrecipMm float64 `json:"precip_mm"` PrecipIn float64 `json:"precip_in"` Humidity int `json:"humidity"` Cloud int `json:"cloud"` FeelsLikeC float64 `json:"feelslike_c"` FeelsLikeF float64 `json:"feelslike_f"` WindChillC float64 `json:"windchill_c"` WindChillF float64 `json:"windchill_f"` HeatIndexC float64 `json:"heatindex_c"` HeatIndexF float64 `json:"heatindex_f"` DewPointC float64 `json:"dewpoint_c"` DewPointF float64 `json:"dewpoint_f"` WillItRain int `json:"will_it_rain"` ChanceOfRain int `json:"chance_of_rain"` WillItSnow int `json:"will_it_snow"` ChanceOfSnow int `json:"chance_of_snow"` VisKm float64 `json:"vis_km"` VisMiles float64 `json:"vis_miles"` GustMph float64 `json:"gust_mph"` GustKph float64 `json:"gust_kph"` UV float64 `json:"uv"` } `json:"hour"` } `json:"forecastday"` } `json:"forecast"` Alerts []struct { Headline string `json:"headline"` MsgType string `json:"msgtype"` Severity string `json:"severity"` Urgency string `json:"urgency"` Areas string `json:"areas"` Category string `json:"category"` Certainty string `json:"certainty"` Event string `json:"event"` Note string `json:"note"` Effective string `json:"effective"` Expires string `json:"expires"` Desc string `json:"desc"` Instruction string `json:"instruction"` } `json:"alerts"` } func NewWeatherService(db *gorm.DB) *WeatherService { apiKey := os.Getenv("WEATHER_API_KEY") baseURL := os.Getenv("WEATHER_API_BASE_URL") if apiKey == "" { apiKey = "20dfd9a556ec43888dc103523250904" // fallback } if baseURL == "" { baseURL = "https://api.weatherapi.com/v1" } return &WeatherService{ db: db, apiKey: apiKey, baseURL: baseURL, client: &http.Client{ Timeout: 10 * time.Second, }, } } func (ws *WeatherService) GetWeatherByLocation(location string) (*WeatherResponse, error) { if location == "" { // Try to get club location from settings location = ws.getClubLocation() if location == "" { return nil, fmt.Errorf("no location specified and no club location found") } } url := fmt.Sprintf("%s/forecast.json?key=%s&q=%s&days=3&aqi=no&alerts=no", ws.baseURL, ws.apiKey, location) resp, err := ws.client.Get(url) if err != nil { return nil, fmt.Errorf("failed to fetch weather data: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("weather API error: status %d, response: %s", resp.StatusCode, string(body)) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } var weatherResp WeatherResponse if err := json.Unmarshal(body, &weatherResp); err != nil { return nil, fmt.Errorf("failed to parse weather response: %w", err) } return &weatherResp, nil } func (ws *WeatherService) getClubLocation() string { var settings models.Settings if err := ws.db.First(&settings).Error; err != nil { return "" } // If coordinates are available, use them for most accurate weather if settings.LocationLatitude != 0 && settings.LocationLongitude != 0 { return fmt.Sprintf("%.6f,%.6f", settings.LocationLatitude, settings.LocationLongitude) } // Try different location fields in order of preference if settings.ContactCity != "" { location := settings.ContactCity if settings.ContactCountry != "" && settings.ContactCountry != "Czech Republic" && settings.ContactCountry != "Česká republika" && settings.ContactCountry != "Česko" { location += "," + settings.ContactCountry } return location } // Fallback to a default Czech city if club is in Czech Republic if settings.ContactCountry == "Czech Republic" || settings.ContactCountry == "Česká republika" || settings.ContactCountry == "Česko" { return "Prague" } return "" } func (ws *WeatherService) GetWeatherForClub() (*WeatherResponse, error) { return ws.GetWeatherByLocation("") } func (ws *WeatherService) GetWeatherForMatch(matchDateTime string, location string) (*WeatherResponse, error) { if location == "" { location = ws.getClubLocation() if location == "" { return nil, fmt.Errorf("no location specified and no club location found") } } // Parse match date to determine if we need hourly forecast matchTime, err := time.Parse("2006-01-02T15:04:05", matchDateTime) if err != nil { // Try alternative format matchTime, err = time.Parse("2006-01-02 15:04:05", matchDateTime) if err != nil { return nil, fmt.Errorf("invalid match date time format: %v", err) } } // If match is more than 7 days away, regular forecast won't be available now := time.Now() if matchTime.Sub(now) > 7*24*time.Hour { return nil, fmt.Errorf("match is too far in the future for weather forecast") } // If match is in the past, no forecast needed if matchTime.Before(now) { return nil, fmt.Errorf("match is in the past") } url := fmt.Sprintf("%s/forecast.json?key=%s&q=%s&days=7&aqi=no&alerts=no", ws.baseURL, ws.apiKey, location) resp, err := ws.client.Get(url) if err != nil { return nil, fmt.Errorf("failed to fetch weather data: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("weather API error: status %d, response: %s", resp.StatusCode, string(body)) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } var weatherResp WeatherResponse if err := json.Unmarshal(body, &weatherResp); err != nil { return nil, fmt.Errorf("failed to parse weather response: %w", err) } return &weatherResp, nil } func (ws *WeatherService) FindClosestHourlyForecast(weather *WeatherResponse, matchTime time.Time) *struct { TimeEpoch int64 `json:"time_epoch"` Time string `json:"time"` TempC float64 `json:"temp_c"` TempF float64 `json:"temp_f"` IsDay int `json:"is_day"` Condition struct { Text string `json:"text"` Icon string `json:"icon"` Code int `json:"code"` } `json:"condition"` WindMph float64 `json:"wind_mph"` WindKph float64 `json:"wind_kph"` WindDegree int `json:"wind_degree"` WindDir string `json:"wind_dir"` PressureMb float64 `json:"pressure_mb"` PressureIn float64 `json:"pressure_in"` PrecipMm float64 `json:"precip_mm"` PrecipIn float64 `json:"precip_in"` Humidity int `json:"humidity"` Cloud int `json:"cloud"` FeelsLikeC float64 `json:"feelslike_c"` FeelsLikeF float64 `json:"feelslike_f"` WindChillC float64 `json:"windchill_c"` WindChillF float64 `json:"windchill_f"` HeatIndexC float64 `json:"heatindex_c"` HeatIndexF float64 `json:"heatindex_f"` DewPointC float64 `json:"dewpoint_c"` DewPointF float64 `json:"dewpoint_f"` WillItRain int `json:"will_it_rain"` ChanceOfRain int `json:"chance_of_rain"` WillItSnow int `json:"will_it_snow"` ChanceOfSnow int `json:"chance_of_snow"` VisKm float64 `json:"vis_km"` VisMiles float64 `json:"vis_miles"` GustMph float64 `json:"gust_mph"` GustKph float64 `json:"gust_kph"` UV float64 `json:"uv"` } { var closestHour *struct { TimeEpoch int64 `json:"time_epoch"` Time string `json:"time"` TempC float64 `json:"temp_c"` TempF float64 `json:"temp_f"` IsDay int `json:"is_day"` Condition struct { Text string `json:"text"` Icon string `json:"icon"` Code int `json:"code"` } `json:"condition"` WindMph float64 `json:"wind_mph"` WindKph float64 `json:"wind_kph"` WindDegree int `json:"wind_degree"` WindDir string `json:"wind_dir"` PressureMb float64 `json:"pressure_mb"` PressureIn float64 `json:"pressure_in"` PrecipMm float64 `json:"precip_mm"` PrecipIn float64 `json:"precip_in"` Humidity int `json:"humidity"` Cloud int `json:"cloud"` FeelsLikeC float64 `json:"feelslike_c"` FeelsLikeF float64 `json:"feelslike_f"` WindChillC float64 `json:"windchill_c"` WindChillF float64 `json:"windchill_f"` HeatIndexC float64 `json:"heatindex_c"` HeatIndexF float64 `json:"heatindex_f"` DewPointC float64 `json:"dewpoint_c"` DewPointF float64 `json:"dewpoint_f"` WillItRain int `json:"will_it_rain"` ChanceOfRain int `json:"chance_of_rain"` WillItSnow int `json:"will_it_snow"` ChanceOfSnow int `json:"chance_of_snow"` VisKm float64 `json:"vis_km"` VisMiles float64 `json:"vis_miles"` GustMph float64 `json:"gust_mph"` GustKph float64 `json:"gust_kph"` UV float64 `json:"uv"` } minDiff := 24 * time.Hour // Start with a large difference for _, day := range weather.Forecast.ForecastDay { for _, hour := range day.Hour { hourTime := time.Unix(hour.TimeEpoch, 0) diff := hourTime.Sub(matchTime) if diff >= 0 && diff < minDiff { minDiff = diff closestHour = &hour } } } return closestHour } func (ws *WeatherService) GetWeatherIconURL(icon string) string { if icon == "" { return "" } // WeatherAPI provides relative URLs, make them absolute return "https:" + icon }