package httpapi import ( "context" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "gitea.maximumdirect.net/ejr/weatherapi/internal/application/alerts" "gitea.maximumdirect.net/ejr/weatherapi/internal/application/conditions" "gitea.maximumdirect.net/ejr/weatherapi/internal/application/forecasts" "gitea.maximumdirect.net/ejr/weatherapi/internal/application/observations" "gitea.maximumdirect.net/ejr/weatherapi/internal/platform/constants" "gitea.maximumdirect.net/ejr/weatherapi/internal/platform/timeparse" ) type ObservationService interface { GetRecent(ctx context.Context, count int, unitSystem string) (observations.Response, error) } type ConditionsService interface { GetCurrent(ctx context.Context, unitSystem string) (conditions.Response, error) } type ForecastService interface { GetByTimestamp(ctx context.Context, ts time.Time, unitSystem string) (forecasts.Response, error) } type AlertService interface { GetCurrent(ctx context.Context) (alerts.Response, error) } type Server struct { obsSvc ObservationService conditionsSvc ConditionsService fcSvc ForecastService alertsSvc AlertService } func NewServer(obsSvc ObservationService, conditionsSvc ConditionsService, fcSvc ForecastService, alertsSvc AlertService) *Server { return &Server{obsSvc: obsSvc, conditionsSvc: conditionsSvc, fcSvc: fcSvc, alertsSvc: alertsSvc} } func (s *Server) Handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/observations", s.handleObservations) mux.HandleFunc("/conditions/current", s.handleConditionsCurrent) mux.HandleFunc("/forecast", s.handleForecast) mux.HandleFunc("/alerts/current", s.handleAlertsCurrent) return mux } func (s *Server) handleConditionsCurrent(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported") return } unitSystem, err := parseUnits(r, constants.UnitSystemUS) if err != nil { writeError(w, http.StatusBadRequest, "invalid_request", err.Error()) return } resp, err := s.conditionsSvc.GetCurrent(r.Context(), unitSystem) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch current conditions") return } writeJSON(w, http.StatusOK, resp) } func (s *Server) handleObservations(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported") return } unitSystem, err := parseUnits(r, constants.UnitSystemMetric) if err != nil { writeError(w, http.StatusBadRequest, "invalid_request", err.Error()) return } count, err := parseCount(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid_request", err.Error()) return } resp, err := s.obsSvc.GetRecent(r.Context(), count, unitSystem) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch observations") return } writeJSON(w, http.StatusOK, resp) } func (s *Server) handleForecast(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported") return } unitSystem, err := parseUnits(r, constants.UnitSystemUS) if err != nil { writeError(w, http.StatusBadRequest, "invalid_request", err.Error()) return } raw := r.URL.Query().Get("timestamp") ts, err := timeparse.ParseTimestamp(raw) if err != nil { writeError(w, http.StatusBadRequest, "invalid_request", err.Error()) return } resp, err := s.fcSvc.GetByTimestamp(r.Context(), ts, unitSystem) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch forecast") return } writeJSON(w, http.StatusOK, resp) } func (s *Server) handleAlertsCurrent(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported") return } resp, err := s.alertsSvc.GetCurrent(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch current alerts") return } writeJSON(w, http.StatusOK, resp) } func parseCount(r *http.Request) (int, error) { raw := strings.TrimSpace(r.URL.Query().Get("count")) if raw == "" { return constants.DefaultObservationCount, nil } count, err := strconv.Atoi(raw) if err != nil { return 0, fmt.Errorf("count must be an integer between 1 and %d", constants.MaxObservationCount) } if count < 1 || count > constants.MaxObservationCount { return 0, fmt.Errorf("count must be an integer between 1 and %d", constants.MaxObservationCount) } return count, nil } func parseUnits(r *http.Request, defaultSystem string) (string, error) { raw := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("units"))) if raw == "" { return defaultSystem, nil } switch raw { case constants.UnitSystemUS, constants.UnitSystemMetric: return raw, nil default: return "", fmt.Errorf("units must be one of: us, metric") } } type errorResponse struct { Code string `json:"code"` Message string `json:"message"` } func writeError(w http.ResponseWriter, status int, code, message string) { writeJSON(w, status, errorResponse{Code: code, Message: message}) } func writeJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(payload) }