package services import ( "encoding/json" "errors" "fmt" "image" _ "image/jpeg" _ "image/png" "math" "net/http" "strings" "time" "fotbal-club/internal/models" "github.com/muesli/clusters" "github.com/muesli/kmeans" ) type FACRService struct { baseURL string httpClient *http.Client } type FACRClubSearchResponse struct { Query string `json:"query"` Count int `json:"count"` Results []struct { Name string `json:"name"` ClubID string `json:"club_id"` ClubType string `json:"club_type"` LogoURL string `json:"logo_url"` Category string `json:"category"` } `json:"results"` } // NewFACRService creates a new FACR service instance func NewFACRService(baseURL string) *FACRService { return &FACRService{ baseURL: strings.TrimSuffix(baseURL, "/"), httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } // SearchClubs searches for clubs by name func (s *FACRService) SearchClubs(query string) ([]models.ClubSearchResult, error) { if query == "" { return nil, errors.New("search query cannot be empty") } url := fmt.Sprintf("%s/club/search?q=%s", s.baseURL, query) resp, err := s.httpClient.Get(url) if err != nil { return nil, fmt.Errorf("error making request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var result FACRClubSearchResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("error decoding response: %v", err) } var clubs []models.ClubSearchResult for _, club := range result.Results { clubs = append(clubs, models.ClubSearchResult{ ID: club.ClubID, Name: club.Name, Type: club.ClubType, LogoURL: club.LogoURL, Category: club.Category, }) } return clubs, nil } // extractColorsFromLogo extracts dominant colors from a logo image func (s *FACRService) extractColorsFromLogo(logoURL string) (string, string, string, error) { resp, err := s.httpClient.Get(logoURL) if err != nil { return "", "", "", fmt.Errorf("error downloading logo: %v", err) } defer resp.Body.Close() img, _, err := image.Decode(resp.Body) if err != nil { return "", "", "", fmt.Errorf("error decoding image: %v", err) } bounds := img.Bounds() var points []clusters.Observation // Sample pixels (every 10th pixel for performance) for y := bounds.Min.Y; y < bounds.Max.Y; y += 10 { for x := bounds.Min.X; x < bounds.Max.X; x += 10 { r, g, b, a := img.At(x, y).RGBA() if a > 0x7FFF { // Skip transparent pixels // Convert to float64 slice and create a new observation point := []float64{ float64(r >> 8), float64(g >> 8), float64(b >> 8), } points = append(points, clusters.Coordinates(point)) } } } if len(points) == 0 { return "#1E40AF", "#F59E0B", "#1F2937", nil } // Find 3 dominant colors using k-means km := kmeans.New() clusters, err := km.Partition(points, 3) // Find 3 dominant colors if err != nil { return "", "", "", fmt.Errorf("error in k-means clustering: %v", err) } // Sort clusters by size (number of points in each cluster) for i := 0; i < len(clusters); i++ { for j := i + 1; j < len(clusters); j++ { if len(clusters[j].Observations) > len(clusters[i].Observations) { clusters[i], clusters[j] = clusters[j], clusters[i] } } } // Get primary and secondary colors primary := clusters[0].Center secondary := clusters[1%len(clusters)].Center // Calculate text color based on brightness brightness := (0.299*primary[0] + 0.587*primary[1] + 0.114*primary[2]) / 255.0 textColor := "#000000" if brightness < 0.5 { textColor = "#FFFFFF" } return rgbToHex(primary), rgbToHex(secondary), textColor, nil } // rgbToHex converts RGB values to hex color func rgbToHex(rgb []float64) string { return fmt.Sprintf("#%02X%02X%02X", uint8(math.Round(rgb[0])), uint8(math.Round(rgb[1])), uint8(math.Round(rgb[2])), ) } // GetClubDetails fetches detailed information about a club func (s *FACRService) GetClubDetails(clubID string) (*models.ClubInfo, error) { if clubID == "" { return nil, errors.New("club ID cannot be empty") } url := fmt.Sprintf("%s/club/football/%s", s.baseURL, clubID) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } resp, err := s.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("error making request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var club struct { Name string `json:"name"` ClubID string `json:"club_id"` LogoURL string `json:"logo_url"` Category string `json:"category"` } if err := json.NewDecoder(resp.Body).Decode(&club); err != nil { return nil, fmt.Errorf("error decoding response: %v", err) } // Extract colors from logo if available var primaryColor, secondaryColor, textColor string var errExtract error if club.LogoURL != "" { primaryColor, secondaryColor, textColor, errExtract = s.extractColorsFromLogo(club.LogoURL) if errExtract != nil { // Log the error but continue with default colors fmt.Printf("Warning: Could not extract colors from logo: %v\n", errExtract) } } // Set default colors if extraction failed or wasn't attempted if primaryColor == "" { primaryColor = "#1E40AF" // Blue secondaryColor = "#F59E0B" // Amber textColor = "#1F2937" // Gray-800 } return &models.ClubInfo{ FACRClubID: club.ClubID, Name: club.Name, ShortName: club.Name, // Using name as fallback for short name LogoURL: club.LogoURL, PrimaryColor: primaryColor, SecondaryColor: secondaryColor, TextColor: textColor, }, nil }