package api import ( "caltrack/ai" "caltrack/calc" "caltrack/config" "caltrack/db" "caltrack/types" "encoding/json" "log" "net/http" "strconv" "time" ) type Handler struct { db *db.DB ai *Client cfg *types.Config } type Client = ai.Client func New(database *db.DB, aiClient *ai.Client, cfg *types.Config) *Handler { return &Handler{ db: database, ai: aiClient, cfg: cfg, } } func (h *Handler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/today", h.handleGetToday) mux.HandleFunc("GET /api/day/{date}", h.handleGetDay) mux.HandleFunc("POST /api/entry", h.handleAddEntry) mux.HandleFunc("DELETE /api/entry/{id}", h.handleDeleteEntry) mux.HandleFunc("DELETE /api/day/{date}", h.handleClearDay) mux.HandleFunc("POST /api/weighin", h.handleWeighIn) mux.HandleFunc("GET /api/autocomplete", h.handleAutocomplete) mux.HandleFunc("GET /api/history", h.handleHistory) mux.HandleFunc("GET /api/weight-history", h.handleWeightHistory) mux.HandleFunc("GET /api/settings", h.handleGetSettings) mux.HandleFunc("POST /api/settings", h.handleUpdateSettings) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) } func (h *Handler) handleGetToday(w http.ResponseWriter, r *http.Request) { h.handleGetDayData(w, time.Now().Format("2006-01-02")) } func (h *Handler) handleGetDay(w http.ResponseWriter, r *http.Request) { date := r.PathValue("date") if date == "" { writeError(w, http.StatusBadRequest, "date is required") return } h.handleGetDayData(w, date) } func (h *Handler) handleGetDayData(w http.ResponseWriter, date string) { entries, err := h.db.GetEntriesByDate(date) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if entries == nil { entries = []types.Entry{} } targetKcal := calc.DailyTarget(*h.cfg) summary, err := h.db.GetDailySummary(date, targetKcal) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } weight, _ := h.db.GetLatestWeight() if weight == 0 { weight = h.cfg.CurrentWeightKg } h.cfg.HasAPIKey = h.cfg.AnthropicAPIKey != "" writeJSON(w, http.StatusOK, types.TodayResponse{ Date: date, Entries: entries, Summary: summary, TargetKcal: targetKcal, Deficit: calc.Deficit(*h.cfg), Weight: weight, Config: *h.cfg, }) } func (h *Handler) handleAddEntry(w http.ResponseWriter, r *http.Request) { var req types.AddEntryRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Description == "" { writeError(w, http.StatusBadRequest, "description is required") return } if req.Type == "" { req.Type = types.EntryFood } // Use AI to estimate calories estimation, err := h.ai.EstimateCalories(r.Context(), req.Description, req.Type) if err != nil { log.Printf("AI estimation error: %v", err) writeError(w, http.StatusInternalServerError, "failed to estimate calories: "+err.Error()) return } var entries []types.Entry for _, item := range estimation.Items { desc := item.Name if item.Brand != "" { desc = item.Name + " (" + item.Brand + ")" } entry := types.Entry{ Type: req.Type, Description: desc, Kcal: item.Kcal, } entry, err = h.db.AddEntry(entry) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } entries = append(entries, entry) } writeJSON(w, http.StatusCreated, types.AddEntryResponse{ Entries: entries, }) } func (h *Handler) handleDeleteEntry(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid entry id") return } if err := h.db.DeleteEntry(id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } func (h *Handler) handleClearDay(w http.ResponseWriter, r *http.Request) { date := r.PathValue("date") if date == "" { writeError(w, http.StatusBadRequest, "date is required") return } if err := h.db.ClearDay(date); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "cleared"}) } func (h *Handler) handleWeighIn(w http.ResponseWriter, r *http.Request) { var req types.WeighInRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.WeightKg <= 0 { writeError(w, http.StatusBadRequest, "weight must be positive") return } weighIn, err := h.db.AddWeighIn("", req.WeightKg) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } // Update config with new weight h.cfg.CurrentWeightKg = req.WeightKg config.Save(*h.cfg) writeJSON(w, http.StatusCreated, weighIn) } func (h *Handler) handleAutocomplete(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") if len(query) < 2 { writeJSON(w, http.StatusOK, []types.AutocompleteResult{}) return } results, err := h.db.SearchProducts(query) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if results == nil { results = []types.AutocompleteResult{} } writeJSON(w, http.StatusOK, results) } func (h *Handler) handleHistory(w http.ResponseWriter, r *http.Request) { summaries, err := h.db.GetHistory(30) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if summaries == nil { summaries = []types.DailySummary{} } writeJSON(w, http.StatusOK, summaries) } func (h *Handler) handleWeightHistory(w http.ResponseWriter, r *http.Request) { weighIns, err := h.db.GetWeightHistory() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if weighIns == nil { weighIns = []types.WeighIn{} } writeJSON(w, http.StatusOK, weighIns) } func (h *Handler) handleGetSettings(w http.ResponseWriter, r *http.Request) { h.cfg.HasAPIKey = h.cfg.AnthropicAPIKey != "" writeJSON(w, http.StatusOK, h.cfg) } func (h *Handler) handleUpdateSettings(w http.ResponseWriter, r *http.Request) { var req types.SettingsRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.CurrentWeightKg != nil { h.cfg.CurrentWeightKg = *req.CurrentWeightKg } if req.TargetWeightKg != nil { h.cfg.TargetWeightKg = *req.TargetWeightKg } if req.HeightCm != nil { h.cfg.HeightCm = *req.HeightCm } if req.Age != nil { h.cfg.Age = *req.Age } if req.Sex != nil { h.cfg.Sex = *req.Sex } if req.ActivityLevel != nil { h.cfg.ActivityLevel = *req.ActivityLevel } if req.WeeklyLossKg != nil { h.cfg.WeeklyLossKg = *req.WeeklyLossKg } if req.AnthropicAPIKey != nil { h.cfg.AnthropicAPIKey = *req.AnthropicAPIKey } if err := config.Save(*h.cfg); err != nil { writeError(w, http.StatusInternalServerError, "failed to save config: "+err.Error()) return } h.cfg.HasAPIKey = h.cfg.AnthropicAPIKey != "" writeJSON(w, http.StatusOK, h.cfg) }