package api import ( "context" "fmt" "io" "log" "net/http" "strings" "time" "github.com/PuerkitoBio/goquery" "task-dashboard/internal/models" ) const ( planToEatBaseURL = "https://www.plantoeat.com/api/v2" planToEatWebURL = "https://app.plantoeat.com" ) // PlanToEatClient handles interactions with the PlanToEat API type PlanToEatClient struct { BaseClient apiKey string sessionCookie string // For web scraping endpoints } // NewPlanToEatClient creates a new PlanToEat API client func NewPlanToEatClient(apiKey string) *PlanToEatClient { return &PlanToEatClient{ BaseClient: NewBaseClient(planToEatBaseURL), apiKey: apiKey, } } // SetSessionCookie sets the session cookie for web scraping endpoints func (c *PlanToEatClient) SetSessionCookie(cookie string) { c.sessionCookie = cookie } func (c *PlanToEatClient) authHeaders() map[string]string { return map[string]string{"Authorization": "Bearer " + c.apiKey} } // 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 by scraping the planner web interface // Requires a valid session cookie set via SetSessionCookie func (c *PlanToEatClient) GetUpcomingMeals(ctx context.Context, days int) ([]models.Meal, error) { if c.sessionCookie == "" { return nil, fmt.Errorf("session cookie required for meals - use SetSessionCookie") } if days <= 0 { days = 7 } req, err := http.NewRequestWithContext(ctx, "GET", planToEatWebURL+"/planner", nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Cookie", c.sessionCookie) req.Header.Set("Accept", "text/html") req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() log.Printf("DEBUG [PlanToEat/Meals]: Response status %d", resp.StatusCode) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status %d (session may be expired)", resp.StatusCode) } return parsePlannerHTML(resp.Body, days) } // parsePlannerHTML extracts meals from the planner page HTML func parsePlannerHTML(body io.Reader, days int) ([]models.Meal, error) { doc, err := goquery.NewDocumentFromReader(body) if err != nil { return nil, fmt.Errorf("failed to parse HTML: %w", err) } // Check for login page title := doc.Find("title").First().Text() titleLower := strings.ToLower(title) log.Printf("DEBUG [PlanToEat/Meals]: Page title: %q", title) if strings.Contains(titleLower, "login") || strings.Contains(titleLower, "log in") || strings.Contains(titleLower, "sign in") { return nil, fmt.Errorf("session expired - got login page (title: %s)", title) } var meals []models.Meal now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) endDate := today.AddDate(0, 0, days) // PlanToEat structure: // - Day cells have data-date="YYYY-MM-DD" // - Within each day, sections: