package integration import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "chorus.services/bzzz/pkg/config" ) // SlurpClient handles HTTP communication with SLURP endpoints type SlurpClient struct { baseURL string apiKey string timeout time.Duration retryCount int retryDelay time.Duration httpClient *http.Client } // SlurpEvent represents a SLURP event structure type SlurpEvent struct { EventType string `json:"event_type"` Path string `json:"path"` Content string `json:"content"` Severity int `json:"severity"` CreatedBy string `json:"created_by"` Metadata map[string]interface{} `json:"metadata"` Tags []string `json:"tags,omitempty"` Timestamp time.Time `json:"timestamp"` } // EventResponse represents the response from SLURP API type EventResponse struct { Success bool `json:"success"` EventID string `json:"event_id,omitempty"` Message string `json:"message,omitempty"` Error string `json:"error,omitempty"` Timestamp time.Time `json:"timestamp"` } // BatchEventRequest represents a batch of events to be sent to SLURP type BatchEventRequest struct { Events []SlurpEvent `json:"events"` Source string `json:"source"` } // BatchEventResponse represents the response for batch event creation type BatchEventResponse struct { Success bool `json:"success"` ProcessedCount int `json:"processed_count"` FailedCount int `json:"failed_count"` EventIDs []string `json:"event_ids,omitempty"` Errors []string `json:"errors,omitempty"` Message string `json:"message,omitempty"` Timestamp time.Time `json:"timestamp"` } // HealthResponse represents SLURP service health status type HealthResponse struct { Status string `json:"status"` Version string `json:"version,omitempty"` Uptime string `json:"uptime,omitempty"` Timestamp time.Time `json:"timestamp"` } // NewSlurpClient creates a new SLURP API client func NewSlurpClient(config config.SlurpConfig) *SlurpClient { return &SlurpClient{ baseURL: strings.TrimSuffix(config.BaseURL, "/"), apiKey: config.APIKey, timeout: config.Timeout, retryCount: config.RetryCount, retryDelay: config.RetryDelay, httpClient: &http.Client{ Timeout: config.Timeout, }, } } // CreateEvent sends a single event to SLURP func (c *SlurpClient) CreateEvent(ctx context.Context, event SlurpEvent) (*EventResponse, error) { url := fmt.Sprintf("%s/api/events", c.baseURL) eventData, err := json.Marshal(event) if err != nil { return nil, fmt.Errorf("failed to marshal event: %w", err) } var lastErr error for attempt := 0; attempt <= c.retryCount; attempt++ { if attempt > 0 { select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(c.retryDelay): } } req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(eventData)) if err != nil { lastErr = fmt.Errorf("failed to create request: %w", err) continue } c.setHeaders(req) resp, err := c.httpClient.Do(req) if err != nil { lastErr = fmt.Errorf("failed to send request: %w", err) continue } defer resp.Body.Close() if c.isRetryableStatus(resp.StatusCode) && attempt < c.retryCount { lastErr = fmt.Errorf("retryable error: HTTP %d", resp.StatusCode) continue } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } var eventResp EventResponse if err := json.Unmarshal(body, &eventResp); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if resp.StatusCode >= 400 { return &eventResp, fmt.Errorf("SLURP API error (HTTP %d): %s", resp.StatusCode, eventResp.Error) } return &eventResp, nil } return nil, fmt.Errorf("failed after %d attempts: %w", c.retryCount+1, lastErr) } // CreateEventsBatch sends multiple events to SLURP in a single request func (c *SlurpClient) CreateEventsBatch(ctx context.Context, events []SlurpEvent) (*BatchEventResponse, error) { url := fmt.Sprintf("%s/api/events/batch", c.baseURL) batchRequest := BatchEventRequest{ Events: events, Source: "bzzz-hmmm-integration", } batchData, err := json.Marshal(batchRequest) if err != nil { return nil, fmt.Errorf("failed to marshal batch request: %w", err) } var lastErr error for attempt := 0; attempt <= c.retryCount; attempt++ { if attempt > 0 { select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(c.retryDelay): } } req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(batchData)) if err != nil { lastErr = fmt.Errorf("failed to create batch request: %w", err) continue } c.setHeaders(req) resp, err := c.httpClient.Do(req) if err != nil { lastErr = fmt.Errorf("failed to send batch request: %w", err) continue } defer resp.Body.Close() if c.isRetryableStatus(resp.StatusCode) && attempt < c.retryCount { lastErr = fmt.Errorf("retryable error: HTTP %d", resp.StatusCode) continue } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read batch response body: %w", err) } var batchResp BatchEventResponse if err := json.Unmarshal(body, &batchResp); err != nil { return nil, fmt.Errorf("failed to unmarshal batch response: %w", err) } if resp.StatusCode >= 400 { return &batchResp, fmt.Errorf("SLURP batch API error (HTTP %d): %s", resp.StatusCode, batchResp.Message) } return &batchResp, nil } return nil, fmt.Errorf("batch failed after %d attempts: %w", c.retryCount+1, lastErr) } // GetHealth checks SLURP service health func (c *SlurpClient) GetHealth(ctx context.Context) (*HealthResponse, error) { url := fmt.Sprintf("%s/api/health", c.baseURL) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create health request: %w", err) } c.setHeaders(req) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send health request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read health response: %w", err) } var healthResp HealthResponse if err := json.Unmarshal(body, &healthResp); err != nil { return nil, fmt.Errorf("failed to unmarshal health response: %w", err) } if resp.StatusCode >= 400 { return &healthResp, fmt.Errorf("SLURP health check failed (HTTP %d)", resp.StatusCode) } return &healthResp, nil } // QueryEvents retrieves events from SLURP based on filters func (c *SlurpClient) QueryEvents(ctx context.Context, filters map[string]string) ([]SlurpEvent, error) { baseURL := fmt.Sprintf("%s/api/events", c.baseURL) // Build query parameters params := url.Values{} for key, value := range filters { params.Add(key, value) } queryURL := baseURL if len(params) > 0 { queryURL = fmt.Sprintf("%s?%s", baseURL, params.Encode()) } req, err := http.NewRequestWithContext(ctx, "GET", queryURL, nil) if err != nil { return nil, fmt.Errorf("failed to create query request: %w", err) } c.setHeaders(req) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send query request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read query response: %w", err) } var events []SlurpEvent if err := json.Unmarshal(body, &events); err != nil { return nil, fmt.Errorf("failed to unmarshal events: %w", err) } if resp.StatusCode >= 400 { return nil, fmt.Errorf("SLURP query failed (HTTP %d)", resp.StatusCode) } return events, nil } // setHeaders sets common HTTP headers for SLURP API requests func (c *SlurpClient) setHeaders(req *http.Request) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "Bzzz-HMMM-Integration/1.0") if c.apiKey != "" { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey)) } } // isRetryableStatus determines if an HTTP status code is retryable func (c *SlurpClient) isRetryableStatus(statusCode int) bool { switch statusCode { case http.StatusTooManyRequests, // 429 http.StatusInternalServerError, // 500 http.StatusBadGateway, // 502 http.StatusServiceUnavailable, // 503 http.StatusGatewayTimeout: // 504 return true default: return false } } // Close cleans up the client resources func (c *SlurpClient) Close() error { // HTTP client doesn't need explicit cleanup, but we can implement // connection pooling cleanup if needed in the future return nil } // ValidateConnection tests the connection to SLURP func (c *SlurpClient) ValidateConnection(ctx context.Context) error { _, err := c.GetHealth(ctx) return err }