package weather import ( "encoding/json" "encoding/xml" "fmt" "io" "net/http" "os" "path/filepath" "strconv" "strings" // ✅ Required for province normalization "time" "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config" ) // WeatherData holds simplified normalized current weather data. type WeatherData struct { Station string `json:"station"` Temperature float64 `json:"temperature"` Condition string `json:"condition"` Humidity string `json:"humidity"` Pressure string `json:"pressure"` WindSpeed string `json:"wind_speed"` WindDir string `json:"wind_dir"` Timestamp string `json:"timestamp"` } // Supported province folders in Environment Canada citypage_weather XML structure. var provinceCodes = []string{ "AB", "BC", "MB", "NB", "NL", "NS", "NT", "NU", "ON", "PE", "QC", "SK", "YT", } // FetchCurrent retrieves current weather from Environment Canada for any province. func FetchCurrent(stationCode, province string) (WeatherData, error) { if stationCode == "" { return WeatherData{}, fmt.Errorf("no station code provided") } // If province unknown, we’ll probe each possible province directory until one succeeds. targetProvinces := provinceCodes if province != "" { targetProvinces = []string{strings.ToUpper(province)} } var lastErr error for _, prov := range targetProvinces { url := fmt.Sprintf("https://dd.weather.gc.ca/citypage_weather/xml/%s/%s_e.xml", prov, stationCode) fmt.Printf("[weather] Fetching current weather for %s in %s...\n", stationCode, prov) client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Get(url) if err != nil { lastErr = err continue } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { lastErr = fmt.Errorf("HTTP %s", resp.Status) continue } body, err := io.ReadAll(resp.Body) if err != nil { return WeatherData{}, fmt.Errorf("failed to read EC XML: %w", err) } var parsed struct { XMLName xml.Name `xml:"siteData"` Location string `xml:"location>name"` CurrentConditions struct { Temperature string `xml:"temperature"` Condition string `xml:"condition"` RelativeHumidity string `xml:"relativeHumidity"` Pressure string `xml:"pressure"` Wind struct { Speed string `xml:"speed"` Direction string `xml:"direction"` } `xml:"wind"` } `xml:"currentConditions"` } if err := xml.Unmarshal(body, &parsed); err != nil { lastErr = fmt.Errorf("failed to parse EC XML: %w", err) continue } if parsed.CurrentConditions.Temperature == "" && parsed.CurrentConditions.Condition == "" { lastErr = fmt.Errorf("no data for %s in %s", stationCode, prov) continue } temp, _ := strconv.ParseFloat(parsed.CurrentConditions.Temperature, 64) data := WeatherData{ Station: parsed.Location, Temperature: temp, Condition: NormalizeCondition(parsed.CurrentConditions.Condition), Humidity: parsed.CurrentConditions.RelativeHumidity, Pressure: parsed.CurrentConditions.Pressure, WindSpeed: parsed.CurrentConditions.Wind.Speed, WindDir: parsed.CurrentConditions.Wind.Direction, Timestamp: time.Now().UTC().Format(time.RFC3339), } if err := SaveToCache(data); err != nil { fmt.Println("[weather] Warning: failed to save cache:", err) } fmt.Printf("[SUCCESS] Current: %.1f°C, %s (%s)\n", data.Temperature, data.Condition, prov) return data, nil } if lastErr != nil { return WeatherData{}, fmt.Errorf("no valid feed found for %s: %v", stationCode, lastErr) } return WeatherData{}, fmt.Errorf("failed to fetch weather for %s", stationCode) } // LoadCached loads the last weather data from disk for offline fallback. func LoadCached() (WeatherData, error) { cachePath := filepath.Join(config.DataDir, "last_weather.json") file, err := os.Open(cachePath) if err != nil { return WeatherData{}, fmt.Errorf("no cached weather found") } defer file.Close() var data WeatherData if err := json.NewDecoder(file).Decode(&data); err != nil { return WeatherData{}, fmt.Errorf("failed to decode cache: %w", err) } fmt.Println("[weather] Loaded cached weather →", data.Timestamp) return data, nil }