package api import ( "context" "encoding/json" "fmt" "io" "net/http" "sync" "time" "task-dashboard/internal/models" ) const ( trelloBaseURL = "https://api.trello.com/1" ) // TrelloClient handles interactions with the Trello API type TrelloClient struct { apiKey string token string httpClient *http.Client } // NewTrelloClient creates a new Trello API client func NewTrelloClient(apiKey, token string) *TrelloClient { return &TrelloClient{ apiKey: apiKey, token: token, httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } // trelloBoardResponse represents a board from the API type trelloBoardResponse struct { ID string `json:"id"` Name string `json:"name"` } // trelloCardResponse represents a card from the API type trelloCardResponse struct { ID string `json:"id"` Name string `json:"name"` IDList string `json:"idList"` Due *string `json:"due"` URL string `json:"url"` Desc string `json:"desc"` IDBoard string `json:"idBoard"` } // trelloListResponse represents a list from the API type trelloListResponse struct { ID string `json:"id"` Name string `json:"name"` } // GetBoards fetches all boards for the authenticated user func (c *TrelloClient) GetBoards(ctx context.Context) ([]models.Board, error) { url := fmt.Sprintf("%s/members/me/boards?key=%s&token=%s", trelloBaseURL, c.apiKey, c.token) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch boards: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body)) } var apiBoards []trelloBoardResponse if err := json.NewDecoder(resp.Body).Decode(&apiBoards); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } // Convert to our model boards := make([]models.Board, 0, len(apiBoards)) for _, apiBoard := range apiBoards { board := models.Board{ ID: apiBoard.ID, Name: apiBoard.Name, Cards: []models.Card{}, // Will be populated by GetCards } boards = append(boards, board) } return boards, nil } // GetCards fetches all cards for a specific board func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.Card, error) { url := fmt.Sprintf("%s/boards/%s/cards?key=%s&token=%s", trelloBaseURL, boardID, c.apiKey, c.token) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch cards: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body)) } var apiCards []trelloCardResponse if err := json.NewDecoder(resp.Body).Decode(&apiCards); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } // Fetch lists to get list names lists, err := c.getLists(ctx, boardID) if err != nil { // If we can't get lists, continue with empty list names lists = make(map[string]string) } // Convert to our model cards := make([]models.Card, 0, len(apiCards)) for _, apiCard := range apiCards { card := models.Card{ ID: apiCard.ID, Name: apiCard.Name, ListID: apiCard.IDList, ListName: lists[apiCard.IDList], URL: apiCard.URL, } // Parse due date if present if apiCard.Due != nil && *apiCard.Due != "" { dueDate, err := time.Parse(time.RFC3339, *apiCard.Due) if err == nil { card.DueDate = &dueDate } } cards = append(cards, card) } return cards, nil } // getLists fetches lists for a board and returns a map of list ID to name func (c *TrelloClient) getLists(ctx context.Context, boardID string) (map[string]string, error) { url := fmt.Sprintf("%s/boards/%s/lists?key=%s&token=%s", trelloBaseURL, boardID, c.apiKey, c.token) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch lists: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body)) } var apiLists []trelloListResponse if err := json.NewDecoder(resp.Body).Decode(&apiLists); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } // Convert to map lists := make(map[string]string, len(apiLists)) for _, list := range apiLists { lists[list.ID] = list.Name } return lists, nil } // GetBoardsWithCards fetches all boards and their cards in one call func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]models.Board, error) { boards, err := c.GetBoards(ctx) if err != nil { return nil, err } var wg sync.WaitGroup sem := make(chan struct{}, 5) // Limit to 5 concurrent requests for i := range boards { wg.Add(1) go func(i int) { defer wg.Done() // Acquire semaphore sem <- struct{}{} defer func() { <-sem }() cards, err := c.GetCards(ctx, boards[i].ID) if err == nil { // It is safe to write to specific indices of the slice concurrently boards[i].Cards = cards } }(i) } wg.Wait() return boards, nil } // CreateCard creates a new card (for Phase 2) func (c *TrelloClient) CreateCard(ctx context.Context, listID, name, description string, dueDate *time.Time) (*models.Card, error) { // This will be implemented in Phase 2 return nil, fmt.Errorf("not implemented yet") } // UpdateCard updates a card (for Phase 2) func (c *TrelloClient) UpdateCard(ctx context.Context, cardID string, updates map[string]interface{}) error { // This will be implemented in Phase 2 return fmt.Errorf("not implemented yet") }