package savedrecipe import ( "context" "encoding/json" "errors" "log/slog" "net/http" "github.com/food-ai/backend/internal/infra/middleware" "github.com/go-chi/chi/v5" ) const maxBodySize = 1 << 20 // 1 MB // SavedRecipeRepository is the data layer interface used by Handler. type SavedRecipeRepository interface { Save(ctx context.Context, userID string, req SaveRequest) (*UserSavedRecipe, error) List(ctx context.Context, userID string) ([]*UserSavedRecipe, error) GetByID(ctx context.Context, userID, id string) (*UserSavedRecipe, error) Delete(ctx context.Context, userID, id string) error } // Handler handles HTTP requests for saved recipes. type Handler struct { repo SavedRecipeRepository } // NewHandler creates a new Handler. func NewHandler(repo SavedRecipeRepository) *Handler { return &Handler{repo: repo} } // Save handles POST /saved-recipes. func (h *Handler) Save(w http.ResponseWriter, r *http.Request) { userID := middleware.UserIDFromCtx(r.Context()) if userID == "" { writeErrorJSON(w, r, http.StatusUnauthorized, "unauthorized") return } r.Body = http.MaxBytesReader(w, r.Body, maxBodySize) var req SaveRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErrorJSON(w, r, http.StatusBadRequest, "invalid request body") return } if req.Title == "" && req.RecipeID == "" { writeErrorJSON(w, r, http.StatusBadRequest, "title or recipe_id is required") return } rec, err := h.repo.Save(r.Context(), userID, req) if err != nil { slog.ErrorContext(r.Context(), "save recipe", "err", err) writeErrorJSON(w, r, http.StatusInternalServerError, "failed to save recipe") return } writeJSON(w, http.StatusCreated, rec) } // List handles GET /saved-recipes. func (h *Handler) List(w http.ResponseWriter, r *http.Request) { userID := middleware.UserIDFromCtx(r.Context()) if userID == "" { writeErrorJSON(w, r, http.StatusUnauthorized, "unauthorized") return } recipes, err := h.repo.List(r.Context(), userID) if err != nil { slog.ErrorContext(r.Context(), "list saved recipes", "err", err) writeErrorJSON(w, r, http.StatusInternalServerError, "failed to list saved recipes") return } if recipes == nil { recipes = []*UserSavedRecipe{} } writeJSON(w, http.StatusOK, recipes) } // GetByID handles GET /saved-recipes/{id}. func (h *Handler) GetByID(w http.ResponseWriter, r *http.Request) { userID := middleware.UserIDFromCtx(r.Context()) if userID == "" { writeErrorJSON(w, r, http.StatusUnauthorized, "unauthorized") return } id := chi.URLParam(r, "id") rec, err := h.repo.GetByID(r.Context(), userID, id) if err != nil { slog.ErrorContext(r.Context(), "get saved recipe", "id", id, "err", err) writeErrorJSON(w, r, http.StatusInternalServerError, "failed to get saved recipe") return } if rec == nil { writeErrorJSON(w, r, http.StatusNotFound, "recipe not found") return } writeJSON(w, http.StatusOK, rec) } // Delete handles DELETE /saved-recipes/{id}. func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { userID := middleware.UserIDFromCtx(r.Context()) if userID == "" { writeErrorJSON(w, r, http.StatusUnauthorized, "unauthorized") return } id := chi.URLParam(r, "id") if err := h.repo.Delete(r.Context(), userID, id); err != nil { if errors.Is(err, ErrNotFound) { writeErrorJSON(w, r, http.StatusNotFound, "recipe not found") return } slog.ErrorContext(r.Context(), "delete saved recipe", "id", id, "err", err) writeErrorJSON(w, r, http.StatusInternalServerError, "failed to delete recipe") return } w.WriteHeader(http.StatusNoContent) } type errorResponse struct { Error string `json:"error"` RequestID string `json:"request_id,omitempty"` } func writeErrorJSON(w http.ResponseWriter, r *http.Request, status int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if encodeErr := json.NewEncoder(w).Encode(errorResponse{ Error: msg, RequestID: middleware.RequestIDFromCtx(r.Context()), }); encodeErr != nil { slog.ErrorContext(r.Context(), "write error response", "err", encodeErr) } } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(v); err != nil { slog.Error("write JSON response", "err", err) } }