package handlers import ( "encoding/json" "log/slog" "net/http" "strconv" "github.com/CubeCraft-Creations/Extrudex/backend/internal/dtos" "github.com/CubeCraft-Creations/Extrudex/backend/internal/models" "github.com/CubeCraft-Creations/Extrudex/backend/internal/repositories" "github.com/CubeCraft-Creations/Extrudex/backend/internal/services" "github.com/go-chi/chi/v5" ) // FilamentHandler handles HTTP requests for filament spool CRUD operations. type FilamentHandler struct { service *services.FilamentService } // NewFilamentHandler creates a FilamentHandler with the given service. func NewFilamentHandler(service *services.FilamentService) *FilamentHandler { return &FilamentHandler{service: service} } // List handles GET /api/filaments — returns paginated, filtered spools. func (h *FilamentHandler) List(w http.ResponseWriter, r *http.Request) { limit, offset := parsePagination(r) filter := repositories.FilamentFilter{ Material: r.URL.Query().Get("material"), Finish: r.URL.Query().Get("finish"), Color: r.URL.Query().Get("color"), LowStock: r.URL.Query().Get("low_stock") == "true", Limit: limit, Offset: offset, } spools, total, err := h.service.List(r.Context(), filter) if err != nil { slog.Error("failed to list filaments", "error", err) writeJSON(w, http.StatusInternalServerError, dtos.ErrorResponse{ Error: "internal server error", Code: http.StatusInternalServerError, }) return } writeJSON(w, http.StatusOK, dtos.ListResponse{ Data: spools, Total: total, Limit: limit, Offset: offset, }) } // Get handles GET /api/filaments/{id} — returns a single spool. func (h *FilamentHandler) Get(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(chi.URLParam(r, "id")) if err != nil { writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{ Error: "invalid filament ID", Code: http.StatusBadRequest, }) return } spool, err := h.service.GetByID(r.Context(), id) if err != nil { slog.Error("failed to get filament", "id", id, "error", err) writeJSON(w, http.StatusInternalServerError, dtos.ErrorResponse{ Error: "internal server error", Code: http.StatusInternalServerError, }) return } if spool == nil { writeJSON(w, http.StatusNotFound, dtos.ErrorResponse{ Error: "filament not found", Code: http.StatusNotFound, }) return } writeJSON(w, http.StatusOK, dtos.SingleResponse{Data: spool}) } // Create handles POST /api/filaments — creates a new filament spool. func (h *FilamentHandler) Create(w http.ResponseWriter, r *http.Request) { var req dtos.CreateFilamentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{ Error: "invalid request body", Code: http.StatusBadRequest, }) return } // Validate required fields. if err := services.ValidateCreateFilamentRequest(req); err != nil { writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{ Error: "validation failed: " + err.Error(), Code: http.StatusBadRequest, }) return } // Build domain model. spool := models.FilamentSpool{ Name: req.Name, MaterialBaseID: req.MaterialBaseID, MaterialFinishID: req.MaterialFinishID, MaterialModifierID: req.MaterialModifierID, ColorHex: req.ColorHex, Brand: req.Brand, DiameterMM: 1.75, // default InitialGrams: req.InitialGrams, RemainingGrams: req.RemainingGrams, SpoolWeightGrams: req.SpoolWeightGrams, CostUSD: req.CostUSD, LowStockThresholdGrams: 50, // default Notes: req.Notes, Barcode: req.Barcode, } if req.DiameterMM != nil { spool.DiameterMM = *req.DiameterMM } if req.LowStockThresholdGrams != nil { spool.LowStockThresholdGrams = *req.LowStockThresholdGrams } created, err := h.service.Create(r.Context(), &spool) if err != nil { slog.Error("failed to create filament", "error", err) writeJSON(w, http.StatusInternalServerError, dtos.ErrorResponse{ Error: "internal server error", Code: http.StatusInternalServerError, }) return } writeJSON(w, http.StatusCreated, dtos.SingleResponse{Data: created}) } // Update handles PUT /api/filaments/{id} — partially updates a spool. func (h *FilamentHandler) Update(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(chi.URLParam(r, "id")) if err != nil { writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{ Error: "invalid filament ID", Code: http.StatusBadRequest, }) return } var req dtos.UpdateFilamentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{ Error: "invalid request body", Code: http.StatusBadRequest, }) return } // Validate update fields. if err := services.ValidateUpdateFilamentRequest(req); err != nil { writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{ Error: "validation failed: " + err.Error(), Code: http.StatusBadRequest, }) return } // Build updates map (only non-nil fields). updates := buildFilamentUpdates(req) updated, err := h.service.Update(r.Context(), id, updates) if err != nil { slog.Error("failed to update filament", "id", id, "error", err) writeJSON(w, http.StatusInternalServerError, dtos.ErrorResponse{ Error: "internal server error", Code: http.StatusInternalServerError, }) return } if updated == nil { writeJSON(w, http.StatusNotFound, dtos.ErrorResponse{ Error: "filament not found", Code: http.StatusNotFound, }) return } writeJSON(w, http.StatusOK, dtos.SingleResponse{Data: updated}) } // Delete handles DELETE /api/filaments/{id} — soft-deletes a spool. func (h *FilamentHandler) Delete(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(chi.URLParam(r, "id")) if err != nil { writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{ Error: "invalid filament ID", Code: http.StatusBadRequest, }) return } deleted, err := h.service.SoftDelete(r.Context(), id) if err != nil { slog.Error("failed to delete filament", "id", id, "error", err) writeJSON(w, http.StatusInternalServerError, dtos.ErrorResponse{ Error: "internal server error", Code: http.StatusInternalServerError, }) return } if !deleted { writeJSON(w, http.StatusNotFound, dtos.ErrorResponse{ Error: "filament not found", Code: http.StatusNotFound, }) return } w.WriteHeader(http.StatusNoContent) } // buildFilamentUpdates converts an UpdateFilamentRequest to a map of column→value. func buildFilamentUpdates(req dtos.UpdateFilamentRequest) map[string]interface{} { updates := make(map[string]interface{}) if req.Name != nil { updates["name"] = *req.Name } if req.MaterialBaseID != nil { updates["material_base_id"] = *req.MaterialBaseID } if req.MaterialFinishID != nil { updates["material_finish_id"] = *req.MaterialFinishID } if req.MaterialModifierID != nil { updates["material_modifier_id"] = *req.MaterialModifierID } if req.ColorHex != nil { updates["color_hex"] = *req.ColorHex } if req.Brand != nil { updates["brand"] = *req.Brand } if req.DiameterMM != nil { updates["diameter_mm"] = *req.DiameterMM } if req.InitialGrams != nil { updates["initial_grams"] = *req.InitialGrams } if req.RemainingGrams != nil { updates["remaining_grams"] = *req.RemainingGrams } if req.SpoolWeightGrams != nil { updates["spool_weight_grams"] = *req.SpoolWeightGrams } if req.CostUSD != nil { updates["cost_usd"] = *req.CostUSD } if req.LowStockThresholdGrams != nil { updates["low_stock_threshold_grams"] = *req.LowStockThresholdGrams } if req.Notes != nil { updates["notes"] = *req.Notes } if req.Barcode != nil { updates["barcode"] = *req.Barcode } return updates }