package client import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "time" "github.com/google/uuid" "github.com/RyanCopley/skybridge/user/internal/domain" ) // UserClient provides an interface to interact with the user service type UserClient struct { baseURL string httpClient *http.Client userEmail string // For authentication } // NewUserClient creates a new user service client func NewUserClient(baseURL, userEmail string) *UserClient { return &UserClient{ baseURL: baseURL, httpClient: &http.Client{ Timeout: 30 * time.Second, }, userEmail: userEmail, } } // NewUserClientWithTimeout creates a new user service client with custom timeout func NewUserClientWithTimeout(baseURL, userEmail string, timeout time.Duration) *UserClient { return &UserClient{ baseURL: baseURL, httpClient: &http.Client{ Timeout: timeout, }, userEmail: userEmail, } } // CreateUser creates a new user func (c *UserClient) CreateUser(req *domain.CreateUserRequest) (*domain.User, error) { body, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } httpReq, err := c.newRequest("POST", "/api/users", bytes.NewBuffer(body)) if err != nil { return nil, err } var user domain.User err = c.doRequest(httpReq, &user) if err != nil { return nil, err } return &user, nil } // GetUserByID retrieves a user by ID func (c *UserClient) GetUserByID(id uuid.UUID) (*domain.User, error) { path := fmt.Sprintf("/api/users/%s", id.String()) httpReq, err := c.newRequest("GET", path, nil) if err != nil { return nil, err } var user domain.User err = c.doRequest(httpReq, &user) if err != nil { return nil, err } return &user, nil } // GetUserByEmail retrieves a user by email func (c *UserClient) GetUserByEmail(email string) (*domain.User, error) { path := fmt.Sprintf("/api/users/email/%s", url.PathEscape(email)) httpReq, err := c.newRequest("GET", path, nil) if err != nil { return nil, err } var user domain.User err = c.doRequest(httpReq, &user) if err != nil { return nil, err } return &user, nil } // UpdateUser updates an existing user func (c *UserClient) UpdateUser(id uuid.UUID, req *domain.UpdateUserRequest) (*domain.User, error) { body, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } path := fmt.Sprintf("/api/users/%s", id.String()) httpReq, err := c.newRequest("PUT", path, bytes.NewBuffer(body)) if err != nil { return nil, err } var user domain.User err = c.doRequest(httpReq, &user) if err != nil { return nil, err } return &user, nil } // DeleteUser deletes a user by ID func (c *UserClient) DeleteUser(id uuid.UUID) error { path := fmt.Sprintf("/api/users/%s", id.String()) httpReq, err := c.newRequest("DELETE", path, nil) if err != nil { return err } return c.doRequest(httpReq, nil) } // ListUsers retrieves a list of users with optional filters func (c *UserClient) ListUsers(req *domain.ListUsersRequest) (*domain.ListUsersResponse, error) { // Build query parameters params := url.Values{} if req.Status != nil { params.Set("status", string(*req.Status)) } if req.Role != nil { params.Set("role", string(*req.Role)) } if req.Search != "" { params.Set("search", req.Search) } if req.Limit > 0 { params.Set("limit", strconv.Itoa(req.Limit)) } if req.Offset > 0 { params.Set("offset", strconv.Itoa(req.Offset)) } if req.OrderBy != "" { params.Set("order_by", req.OrderBy) } if req.OrderDir != "" { params.Set("order_dir", req.OrderDir) } path := "/api/users" if len(params) > 0 { path += "?" + params.Encode() } httpReq, err := c.newRequest("GET", path, nil) if err != nil { return nil, err } var response domain.ListUsersResponse err = c.doRequest(httpReq, &response) if err != nil { return nil, err } return &response, nil } // ExistsByEmail checks if a user exists with the given email func (c *UserClient) ExistsByEmail(email string) (bool, error) { path := fmt.Sprintf("/api/users/exists/%s", url.PathEscape(email)) httpReq, err := c.newRequest("GET", path, nil) if err != nil { return false, err } var response map[string]interface{} err = c.doRequest(httpReq, &response) if err != nil { return false, err } exists, ok := response["exists"].(bool) if !ok { return false, fmt.Errorf("invalid response format") } return exists, nil } // Health checks the health of the user service func (c *UserClient) Health() (map[string]interface{}, error) { httpReq, err := c.newRequest("GET", "/health", nil) if err != nil { return nil, err } var response map[string]interface{} err = c.doRequest(httpReq, &response) if err != nil { return nil, err } return response, nil } // newRequest creates a new HTTP request with authentication headers func (c *UserClient) newRequest(method, path string, body io.Reader) (*http.Request, error) { url := c.baseURL + path req, err := http.NewRequest(method, url, body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") // Add authentication header if c.userEmail != "" { req.Header.Set("X-User-Email", c.userEmail) } return req, nil } // doRequest executes an HTTP request and handles the response func (c *UserClient) doRequest(req *http.Request, target interface{}) error { resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to execute request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode >= 400 { var errorResponse map[string]interface{} if json.Unmarshal(body, &errorResponse) == nil { if errorMsg, ok := errorResponse["error"].(string); ok { return fmt.Errorf("API error (status %d): %s", resp.StatusCode, errorMsg) } } return fmt.Errorf("HTTP error (status %d): %s", resp.StatusCode, string(body)) } // If target is nil, we don't need to unmarshal (e.g., for DELETE requests) if target == nil { return nil } if err := json.Unmarshal(body, target); err != nil { return fmt.Errorf("failed to unmarshal response: %w", err) } return nil }