package event import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "slices" "strconv" "sync" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" ) type service struct { token string store *repository.Store } func New(token string, store *repository.Store) Service { return &service{ token: token, store: store, } } func (s *service) FetchLiveEvents(ctx context.Context) error { var wg sync.WaitGroup urls := []struct { name string source string }{ {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", "bet365"}, {"https://api.b365api.com/v1/betfair/sb/inplay?sport_id=%d&token=%s", "betfair"}, {"https://api.b365api.com/v1/1xbet/inplay?sport_id=%d&token=%s", "1xbet"}, } for _, url := range urls { wg.Add(1) go func() { defer wg.Done() s.fetchLiveEvents(ctx, url.name, url.source) }() } wg.Wait() return nil } func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error { sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148} var wg sync.WaitGroup for _, sportID := range sportIDs { wg.Add(1) go func(sportID int) { defer wg.Done() url := fmt.Sprintf(url, sportID, s.token) resp, err := http.Get(url) if err != nil { fmt.Printf(" Failed request for sport_id=%d: %v\n", sportID, err) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) events := []domain.Event{} switch source { case "bet365": events = handleBet365prematch(body, sportID, source) case "betfair": events = handleBetfairprematch(body, sportID, source) case "1xbet": // betfair and 1xbet have the same result structure events = handleBetfairprematch(body, sportID, source) } for _, event := range events { if err := s.store.SaveEvent(ctx, event); err != nil { fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) } } }(sportID) } wg.Wait() fmt.Println("All live events fetched and stored.") return nil } func handleBet365prematch(body []byte, sportID int, source string) []domain.Event { var data struct { Success int `json:"success"` Results [][]map[string]interface{} `json:"results"` } events := []domain.Event{} if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) return events } for _, group := range data.Results { for _, ev := range group { if getString(ev["type"]) != "EV" { continue } event := domain.Event{ ID: getString(ev["ID"]), SportID: int32(sportID), MatchName: getString(ev["NA"]), Score: getString(ev["SS"]), MatchMinute: getInt(ev["TM"]), TimerStatus: getString(ev["TT"]), HomeTeamID: getInt32(ev["HT"]), AwayTeamID: getInt32(ev["AT"]), HomeKitImage: getString(ev["K1"]), AwayKitImage: getString(ev["K2"]), LeagueName: getString(ev["CT"]), LeagueID: getInt32(ev["C2"]), LeagueCC: getString(ev["CB"]), StartTime: time.Now().UTC().Format(time.RFC3339), IsLive: true, Status: "live", MatchPeriod: getInt(ev["MD"]), AddedTime: getInt(ev["TA"]), Source: source, } events = append(events, event) } } return events } func handleBetfairprematch(body []byte, sportID int, source string) []domain.Event { var data struct { Success int `json:"success"` Results []map[string]interface{} `json:"results"` } events := []domain.Event{} if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) return events } for _, ev := range data.Results { homeRaw, _ := ev["home"].(map[string]interface{}) awayRaw, _ := ev["home"].(map[string]interface{}) event := domain.Event{ ID: getString(ev["id"]), SportID: int32(sportID), TimerStatus: getString(ev["time_status"]), HomeTeamID: getInt32(homeRaw["id"]), AwayTeamID: getInt32(awayRaw["id"]), StartTime: time.Now().UTC().Format(time.RFC3339), IsLive: true, Status: "live", Source: source, } events = append(events, event) } return events } func (s *service) FetchUpcomingEvents(ctx context.Context) error { var wg sync.WaitGroup urls := []struct { name string source string }{ {"https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", "bet365"}, // {"https://api.b365api.com/v1/betfair/sb/upcoming?sport_id=%d&token=%s&page=%d", "betfair"}, // {"https://api.b365api.com/v1/1xbet/upcoming?sport_id=%d&token=%s&page=%d", "1xbet"}, } for _, url := range urls { wg.Add(1) go func() { defer wg.Done() s.fetchUpcomingEventsFromProvider(ctx, url.name, url.source) }() } wg.Wait() return nil } func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) { sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} // sportIDs := []int{1} // TODO: Add the league skipping again when we have dynamic leagues // b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) // if err != nil { // log.Printf("❌ Failed to open leagues file %v", err) // return // } for sportIndex, sportID := range sportIDs { var totalPages int = 1 var page int = 0 var limit int = 200 var count int = 0 for page <= totalPages { page = page + 1 url := fmt.Sprintf(url, sportID, s.token, page) log.Printf("📡 Fetching data from %s - sport %d (%d/%d), for event data page (%d/%d)", source, sportID, sportIndex+1, len(sportIDs), page, totalPages) resp, err := http.Get(url) if err != nil { log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) continue } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var data domain.BetResult if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { log.Printf("❌ Failed to parse json data") continue } var skippedLeague []string for _, ev := range data.Results { startUnix, _ := strconv.ParseInt(ev.Time, 10, 64) // eventID, err := strconv.ParseInt(ev.ID, 10, 64) // if err != nil { // log.Panicf("❌ Invalid event id, eventID %v", ev.ID) // continue // } leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64) if err != nil { log.Printf("❌ Invalid league id, leagueID %v", ev.League.ID) continue } // doesn't make sense to save and check back to back, but for now it can be here // no this its fine to keep it here // but change the league id to bet365 id later //Automatically feature the league if its in the list err = s.store.SaveLeague(ctx, domain.League{ ID: leagueID, Name: ev.League.Name, IsActive: true, IsFeatured: slices.Contains(domain.FeaturedLeagues, leagueID), SportID: convertInt32(ev.SportID), }) if err != nil { log.Printf("❌ Error Saving League on %v", ev.League.Name) log.Printf("err:%v", err) continue } if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil { log.Printf("Skipping league %v", ev.League.Name) skippedLeague = append(skippedLeague, ev.League.Name) continue } event := domain.UpcomingEvent{ ID: ev.ID, SportID: convertInt32(ev.SportID), MatchName: "", HomeTeam: ev.Home.Name, AwayTeam: "", // handle nil safely HomeTeamID: convertInt32(ev.Home.ID), AwayTeamID: 0, HomeKitImage: "", AwayKitImage: "", LeagueID: convertInt32(ev.League.ID), LeagueName: ev.League.Name, LeagueCC: "", StartTime: time.Unix(startUnix, 0).UTC(), Source: source, } if ev.Away != nil { event.AwayTeam = ev.Away.Name event.AwayTeamID = convertInt32(ev.Away.ID) event.MatchName = ev.Home.Name + " vs " + ev.Away.Name } err = s.store.SaveUpcomingEvent(ctx, event) if err != nil { log.Printf("❌ Failed to save upcoming event %s", event.ID) } } log.Printf("⚠️ Skipped leagues %v", len(skippedLeague)) log.Printf("⚠️ Total pages %v", data.Pager.Total/data.Pager.PerPage) totalPages = data.Pager.Total / data.Pager.PerPage if count >= limit { break } if page > totalPages { break } count++ } } } func getString(v interface{}) string { if str, ok := v.(string); ok { return str } return "" } func getInt(v interface{}) int { if f, ok := v.(float64); ok { return int(f) } return 0 } func getInt32(v interface{}) int32 { if n, err := strconv.Atoi(getString(v)); err == nil { return int32(n) } return 0 } func convertInt32(num string) int32 { if n, err := strconv.Atoi(num); err == nil { return int32(n) } return 0 } func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { return s.store.GetAllUpcomingEvents(ctx) } func (s *service) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) { return s.store.GetExpiredUpcomingEvents(ctx, filter) } func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) { return s.store.GetPaginatedUpcomingEvents(ctx, filter) } func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { return s.store.GetUpcomingEventByID(ctx, ID) } func (s *service) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { return s.store.UpdateFinalScore(ctx, eventID, fullScore, status) } func (s *service) UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error { return s.store.UpdateEventStatus(ctx, eventID, status) } // func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error { // url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID) // resp, err := http.Get(url) // if err != nil { // return fmt.Errorf("failed to fetch result: %w", err) // } // defer resp.Body.Close() // body, _ := io.ReadAll(resp.Body) // // Parse the API response // var apiResp struct { // Results []struct { // ID string `json:"id"` // Ss string `json:"ss"` // Full-time score // Status string `json:"time_status"` // } `json:"results"` // } // err = json.Unmarshal(body, &apiResp) // if err != nil || len(apiResp.Results) == 0 { // return fmt.Errorf("invalid response or no results found") // } // result := apiResp.Results[0] // err = s.store.UpdateFinalScore(ctx, result.ID, result.Ss, result.Status) // if err != nil { // return fmt.Errorf("failed to update final score in database: %w", err) // } // return nil // }