package handler import ( "context" "net/http" "time" "predictor-refactored/internal/prediction" api "predictor-refactored/pkg/rest" "go.uber.org/zap" ) var _ api.Handler = (*Handler)(nil) // Handler implements the ogen-generated api.Handler interface. type Handler struct { svc Service log *zap.Logger } // New creates a new Handler. func New(svc Service, log *zap.Logger) *Handler { return &Handler{svc: svc, log: log} } // PerformPrediction implements the prediction endpoint. func (h *Handler) PerformPrediction(ctx context.Context, params api.PerformPredictionParams) (*api.PredictionResponse, error) { if !h.svc.Ready() { return nil, newError(http.StatusServiceUnavailable, "no dataset loaded, service is starting up") } ds := h.svc.Dataset() if ds == nil { return nil, newError(http.StatusServiceUnavailable, "dataset unavailable") } dsEpoch := float64(ds.DSTime.Unix()) // Parse parameters with defaults profile := "standard_profile" if p, ok := params.Profile.Get(); ok { profile = string(p) } ascentRate := 5.0 if v, ok := params.AscentRate.Get(); ok { ascentRate = v } burstAltitude := 28000.0 if v, ok := params.BurstAltitude.Get(); ok { burstAltitude = v } descentRate := 5.0 if v, ok := params.DescentRate.Get(); ok { descentRate = v } launchAlt := 0.0 if v, ok := params.LaunchAltitude.Get(); ok { launchAlt = v } // Normalize longitude to [0, 360) lng := params.LaunchLongitude if lng < 0 { lng += 360.0 } launchTime := float64(params.LaunchDatetime.Unix()) warnings := &prediction.Warnings{} // Build profile chain elev := h.svc.Elevation() var stages []prediction.Stage switch profile { case "standard_profile": stages = prediction.StandardProfile( ascentRate, burstAltitude, descentRate, ds, dsEpoch, warnings, elev) case "float_profile": floatAlt := 25000.0 if v, ok := params.FloatAltitude.Get(); ok { floatAlt = v } stopTime := params.LaunchDatetime.Add(24 * time.Hour) if v, ok := params.StopDatetime.Get(); ok { stopTime = v } stages = prediction.FloatProfile( ascentRate, floatAlt, stopTime, ds, dsEpoch, warnings) default: return nil, newError(http.StatusBadRequest, "unknown profile: "+profile) } // Run prediction startTime := time.Now().UTC() results := prediction.RunPrediction(launchTime, params.LaunchLatitude, lng, launchAlt, stages) completeTime := time.Now().UTC() // Build response stageNames := []string{"ascent", "descent"} if profile == "float_profile" { stageNames = []string{"ascent", "float"} } var predItems []api.PredictionResponsePredictionItem for i, sr := range results { stageName := "ascent" if i < len(stageNames) { stageName = stageNames[i] } var stageEnum api.PredictionResponsePredictionItemStage switch stageName { case "ascent": stageEnum = api.PredictionResponsePredictionItemStageAscent case "descent": stageEnum = api.PredictionResponsePredictionItemStageDescent case "float": stageEnum = api.PredictionResponsePredictionItemStageFloat } var traj []api.PredictionResponsePredictionItemTrajectoryItem for _, pt := range sr.Points { ptLng := pt.Lng if ptLng > 180 { ptLng -= 360 } traj = append(traj, api.PredictionResponsePredictionItemTrajectoryItem{ Datetime: time.Unix(int64(pt.T), 0).UTC(), Latitude: pt.Lat, Longitude: ptLng, Altitude: pt.Alt, }) } predItems = append(predItems, api.PredictionResponsePredictionItem{ Stage: stageEnum, Trajectory: traj, }) } resp := &api.PredictionResponse{ Prediction: predItems, Metadata: api.PredictionResponseMetadata{ StartDatetime: startTime, CompleteDatetime: completeTime, }, } // Echo request resp.Request = api.NewOptPredictionResponseRequest(api.PredictionResponseRequest{ Dataset: api.NewOptString(ds.DSTime.Format("2006-01-02T15:04:05Z")), LaunchLatitude: api.NewOptFloat64(params.LaunchLatitude), LaunchLongitude: api.NewOptFloat64(params.LaunchLongitude), LaunchDatetime: api.NewOptString(params.LaunchDatetime.Format(time.RFC3339)), LaunchAltitude: params.LaunchAltitude, }) // Warnings warnMap := warnings.ToMap() if len(warnMap) > 0 { resp.Warnings = api.NewOptPredictionResponseWarnings(api.PredictionResponseWarnings{}) } h.log.Info("prediction complete", zap.String("profile", profile), zap.Int("stages", len(results)), zap.Duration("elapsed", completeTime.Sub(startTime))) return resp, nil } // ReadinessCheck implements the health check endpoint. func (h *Handler) ReadinessCheck(ctx context.Context) (*api.ReadinessResponse, error) { resp := &api.ReadinessResponse{} if h.svc.Ready() { resp.Status = api.ReadinessResponseStatusOk if dsTime, ok := h.svc.DatasetTime(); ok { resp.DatasetTime = api.NewOptDateTime(dsTime) } } else { resp.Status = api.ReadinessResponseStatusNotReady resp.ErrorMessage = api.NewOptString("no dataset loaded") } return resp, nil } // NewError creates an ErrorStatusCode from an error returned by a handler. func (h *Handler) NewError(ctx context.Context, err error) *api.ErrorStatusCode { if statusErr, ok := err.(*api.ErrorStatusCode); ok { return statusErr } h.log.Error("unhandled error", zap.Error(err)) return newError(http.StatusInternalServerError, err.Error()) } func newError(status int, description string) *api.ErrorStatusCode { return &api.ErrorStatusCode{ StatusCode: status, Response: api.Error{ Error: api.ErrorError{ Type: http.StatusText(status), Description: description, }, }, } }