package api import ( "context" "encoding/json" "fmt" "io" "net/http" "time" "task-dashboard/internal/models" ) const ( todoistBaseURL = "https://api.todoist.com/rest/v2" ) // TodoistClient handles interactions with the Todoist API type TodoistClient struct { apiKey string httpClient *http.Client } // NewTodoistClient creates a new Todoist API client func NewTodoistClient(apiKey string) *TodoistClient { return &TodoistClient{ apiKey: apiKey, httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } // todoistTaskResponse represents the API response structure type todoistTaskResponse struct { ID string `json:"id"` Content string `json:"content"` Description string `json:"description"` ProjectID string `json:"project_id"` Priority int `json:"priority"` Labels []string `json:"labels"` Due *struct { Date string `json:"date"` Datetime string `json:"datetime"` } `json:"due"` URL string `json:"url"` CreatedAt string `json:"created_at"` } // todoistProjectResponse represents the project API response type todoistProjectResponse struct { ID string `json:"id"` Name string `json:"name"` } // GetTasks fetches all active tasks from Todoist func (c *TodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) { req, err := http.NewRequestWithContext(ctx, "GET", todoistBaseURL+"/tasks", nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.apiKey) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch tasks: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("todoist API error (status %d): %s", resp.StatusCode, string(body)) } var apiTasks []todoistTaskResponse if err := json.NewDecoder(resp.Body).Decode(&apiTasks); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } // Fetch projects to get project names projects, err := c.GetProjects(ctx) if err != nil { // If we can't get projects, continue with empty project names projects = make(map[string]string) } // Convert to our model tasks := make([]models.Task, 0, len(apiTasks)) for _, apiTask := range apiTasks { task := models.Task{ ID: apiTask.ID, Content: apiTask.Content, Description: apiTask.Description, ProjectID: apiTask.ProjectID, ProjectName: projects[apiTask.ProjectID], Priority: apiTask.Priority, Completed: false, Labels: apiTask.Labels, URL: apiTask.URL, } // Parse created_at if createdAt, err := time.Parse(time.RFC3339, apiTask.CreatedAt); err == nil { task.CreatedAt = createdAt } // Parse due date if apiTask.Due != nil { var dueDate time.Time if apiTask.Due.Datetime != "" { dueDate, err = time.Parse(time.RFC3339, apiTask.Due.Datetime) } else if apiTask.Due.Date != "" { dueDate, err = time.Parse("2006-01-02", apiTask.Due.Date) } if err == nil { task.DueDate = &dueDate } } tasks = append(tasks, task) } return tasks, nil } // GetProjects fetches all projects and returns a map of project ID to name func (c *TodoistClient) GetProjects(ctx context.Context) (map[string]string, error) { req, err := http.NewRequestWithContext(ctx, "GET", todoistBaseURL+"/projects", nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.apiKey) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch projects: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("todoist API error (status %d): %s", resp.StatusCode, string(body)) } var apiProjects []todoistProjectResponse if err := json.NewDecoder(resp.Body).Decode(&apiProjects); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } // Convert to map projects := make(map[string]string, len(apiProjects)) for _, project := range apiProjects { projects[project.ID] = project.Name } return projects, nil } // CreateTask creates a new task in Todoist func (c *TodoistClient) CreateTask(ctx context.Context, content, projectID string, dueDate *time.Time, priority int) (*models.Task, error) { // This will be implemented in Phase 2 return nil, fmt.Errorf("not implemented yet") } // CompleteTask marks a task as complete in Todoist func (c *TodoistClient) CompleteTask(ctx context.Context, taskID string) error { // This will be implemented in Phase 2 return fmt.Errorf("not implemented yet") }