package handlers import ( "context" "fmt" "math" "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/pkg/slapshotapi" "github.com/pkg/errors" "github.com/uptrace/bun" ) // PlayerLookupResult stores the resolved player info from a game_user_id lookup type PlayerLookupResult struct { Player *db.Player TeamID int Found bool Unmapped bool // true if player not in system (potential free agent) } // MapGameUserIDsToPlayers creates a lookup map from game_user_id to resolved player info. // It looks up players by their SlapID (which corresponds to game_user_id in match logs) // and checks their team assignment in the given season/league. func MapGameUserIDsToPlayers( ctx context.Context, tx bun.Tx, gameUserIDs []string, seasonID, leagueID int, ) (map[string]*PlayerLookupResult, error) { result := make(map[string]*PlayerLookupResult, len(gameUserIDs)) // Initialize all as unmapped for _, id := range gameUserIDs { result[id] = &PlayerLookupResult{Unmapped: true} } if len(gameUserIDs) == 0 { return result, nil } // Get all players that have a slap_id matching any of the game_user_ids // game_user_id in logs is a string representation of the slapshot player ID (uint32) players := []*db.Player{} err := tx.NewSelect(). Model(&players). Where("p.slap_id::text IN (?)", bun.In(gameUserIDs)). Relation("User"). Scan(ctx) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect players") } // Build a map of slapID -> player slapIDToPlayer := make(map[string]*db.Player, len(players)) playerIDs := make([]int, 0, len(players)) for _, p := range players { if p.SlapID != nil { key := slapIDStr(*p.SlapID) slapIDToPlayer[key] = p playerIDs = append(playerIDs, p.ID) } } // Get team roster entries for these players in the given season/league rosters := []*db.TeamRoster{} if len(playerIDs) > 0 { err = tx.NewSelect(). Model(&rosters). Where("tr.season_id = ?", seasonID). Where("tr.league_id = ?", leagueID). Where("tr.player_id IN (?)", bun.In(playerIDs)). Scan(ctx) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect rosters") } } // Build playerID -> teamID map playerTeam := make(map[int]int, len(rosters)) for _, r := range rosters { playerTeam[r.PlayerID] = r.TeamID } // Populate results for _, id := range gameUserIDs { player, found := slapIDToPlayer[id] if !found { continue // stays unmapped } teamID, onTeam := playerTeam[player.ID] result[id] = &PlayerLookupResult{ Player: player, TeamID: teamID, Found: true, Unmapped: !onTeam, } } return result, nil } // DetermineTeamOrientation validates that logs match fixture's team assignment // by cross-checking player game_user_ids against registered rosters. // // Returns: // - fixtureHomeIsLogsHome: true if fixture's home team maps to "home" in logs // - unmappedPlayers: list of game_user_ids that couldn't be resolved // - error: if orientation cannot be determined func DetermineTeamOrientation( ctx context.Context, tx bun.Tx, fixture *db.Fixture, allPlayers []slapshotapi.Player, playerLookup map[string]*PlayerLookupResult, ) (bool, []string, error) { if fixture == nil { return false, nil, errors.New("fixture cannot be nil") } unmapped := []string{} // Count how many fixture-home-team players are on "home" vs "away" in logs homeTeamOnHome := 0 // fixture home team players that are "home" in logs homeTeamOnAway := 0 // fixture home team players that are "away" in logs awayTeamOnHome := 0 // fixture away team players that are "home" in logs awayTeamOnAway := 0 // fixture away team players that are "away" in logs for _, p := range allPlayers { lookup, exists := playerLookup[p.GameUserID] if !exists || lookup.Unmapped { unmapped = append(unmapped, p.GameUserID+" ("+p.Username+")") continue } logTeam := p.Team // "home" or "away" in the log switch lookup.TeamID { case fixture.HomeTeamID: if logTeam == "home" { homeTeamOnHome++ } else { homeTeamOnAway++ } case fixture.AwayTeamID: if logTeam == "home" { awayTeamOnHome++ } else { awayTeamOnAway++ } default: // Player is on a team but not one of the fixture teams unmapped = append(unmapped, p.GameUserID+" ("+p.Username+")") } } totalMapped := homeTeamOnHome + homeTeamOnAway + awayTeamOnHome + awayTeamOnAway if totalMapped == 0 { return false, unmapped, errors.New("no mapped players found, cannot determine team orientation") } // Calculate orientation: how many agree with "home=home" vs "home=away" matchOrientation := homeTeamOnHome + awayTeamOnAway // logs match fixture orientation reverseOrientation := homeTeamOnAway + awayTeamOnHome // logs are reversed if matchOrientation == reverseOrientation { return false, unmapped, errors.New("cannot determine team orientation: equal evidence for both orientations") } fixtureHomeIsLogsHome := matchOrientation > reverseOrientation return fixtureHomeIsLogsHome, unmapped, nil } // FloatToIntPtr converts a *float64 to *int by truncating the decimal. // Returns nil if input is nil. func FloatToIntPtr(f *float64) *int { if f == nil { return nil } v := int(math.Round(*f)) return &v } // slapIDStr converts a uint32 SlapID to a string for map lookups func slapIDStr(id uint32) string { return fmt.Sprintf("%d", id) }