summaryrefslogtreecommitdiff
path: root/internal/api/plantoeat.go
blob: 1dae2466b51010d5dc3654fa34f573ad2d4ce6a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package api

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	"task-dashboard/internal/models"
)

const (
	planToEatBaseURL = "https://www.plantoeat.com/api/v2"
)

// PlanToEatClient handles interactions with the PlanToEat API
type PlanToEatClient struct {
	apiKey     string
	httpClient *http.Client
}

// NewPlanToEatClient creates a new PlanToEat API client
func NewPlanToEatClient(apiKey string) *PlanToEatClient {
	return &PlanToEatClient{
		apiKey: apiKey,
		httpClient: &http.Client{
			Timeout: 15 * time.Second,
		},
	}
}

// planToEatPlannerItem represents a planner item from the API
type planToEatPlannerItem struct {
	ID       int    `json:"id"`
	Date     string `json:"date"`
	MealType string `json:"meal_type"`
	Recipe   struct {
		ID    int    `json:"id"`
		Title string `json:"title"`
		URL   string `json:"url"`
	} `json:"recipe"`
}

// planToEatResponse wraps the API response
type planToEatResponse struct {
	Items []planToEatPlannerItem `json:"items"`
}

// GetUpcomingMeals fetches meals for the next N days
func (c *PlanToEatClient) GetUpcomingMeals(ctx context.Context, days int) ([]models.Meal, error) {
	if days <= 0 {
		days = 7 // Default to 7 days
	}

	startDate := time.Now()
	endDate := startDate.AddDate(0, 0, days)

	req, err := http.NewRequestWithContext(ctx, "GET", planToEatBaseURL+"/planner_items", nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	// Add query parameters
	q := req.URL.Query()
	q.Add("start_date", startDate.Format("2006-01-02"))
	q.Add("end_date", endDate.Format("2006-01-02"))
	req.URL.RawQuery = q.Encode()

	// Add API key (check docs for correct header name)
	req.Header.Set("Authorization", "Bearer "+c.apiKey)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch meals: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("plantoeat API error (status %d): %s", resp.StatusCode, string(body))
	}

	var apiResponse planToEatResponse
	if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	// Convert to our model
	meals := make([]models.Meal, 0, len(apiResponse.Items))
	for _, item := range apiResponse.Items {
		mealDate, err := time.Parse("2006-01-02", item.Date)
		if err != nil {
			continue // Skip invalid dates
		}

		meal := models.Meal{
			ID:         fmt.Sprintf("%d", item.ID),
			RecipeName: item.Recipe.Title,
			Date:       mealDate,
			MealType:   normalizeMealType(item.MealType),
			RecipeURL:  item.Recipe.URL,
		}

		meals = append(meals, meal)
	}

	return meals, nil
}

// normalizeMealType ensures meal type matches our expected values
func normalizeMealType(mealType string) string {
	switch mealType {
	case "breakfast", "Breakfast":
		return "breakfast"
	case "lunch", "Lunch":
		return "lunch"
	case "dinner", "Dinner":
		return "dinner"
	case "snack", "Snack":
		return "snack"
	default:
		return "dinner" // Default to dinner
	}
}

// GetRecipes fetches recipes (for Phase 2)
func (c *PlanToEatClient) GetRecipes(ctx context.Context) error {
	// This will be implemented in Phase 2
	return fmt.Errorf("not implemented yet")
}

// AddMealToPlanner adds a meal to the planner (for Phase 2)
func (c *PlanToEatClient) AddMealToPlanner(ctx context.Context, recipeID string, date time.Time, mealType string) error {
	// This will be implemented in Phase 2
	return fmt.Errorf("not implemented yet")
}