package main import ( "net/http" "strconv" "github.com/x0gp/server/internal/races" "github.com/x0gp/server/internal/transport" ) // leaderboardHandler godoc // @Summary Leaderboard // @Description Returns paginated leaderboard list. Supports overall leaderboard or track-specific leaderboard for the current year. // @Tags leaderboard // @Produce json // @Param track_id query string false "Track ID. If empty, returns overall leaderboard." // @Param year query int false "Year. Defaults to current year." // @Param limit query int false "Limit (1..200, default 50)" // @Param offset query int false "Offset (default 0)" // @Param X-Driver-Id header string false "Current driver ID. Defaults to driver-seed-001 (mock Alice)." // @Success 200 {object} transport.LeaderboardResponse "Leaderboard page and current driver position" // @Failure 400 {object} map[string]interface{} "Invalid input" // @Failure 500 {object} map[string]interface{} "Internal server error" // @Router /api/leaderboard [get] func leaderboardHandler(svc *races.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { trackID := r.URL.Query().Get("track_id") yearVal := r.URL.Query().Get("year") var year int if yearVal != "" { var err error year, err = strconv.Atoi(yearVal) if err != nil { writeError(w, http.StatusBadRequest, "invalid_input", "invalid year") return } } limit := 50 limitVal := r.URL.Query().Get("limit") if limitVal != "" { var err error limit, err = strconv.Atoi(limitVal) if err != nil || limit <= 0 { writeError(w, http.StatusBadRequest, "invalid_input", "invalid limit") return } if limit > 200 { limit = 200 } } offset := 0 offsetVal := r.URL.Query().Get("offset") if offsetVal != "" { var err error offset, err = strconv.Atoi(offsetVal) if err != nil || offset < 0 { writeError(w, http.StatusBadRequest, "invalid_input", "invalid offset") return } } driverID := r.Header.Get("X-Driver-Id") if driverID == "" { driverID = "driver-seed-001" // mock current driver } res, err := svc.GetLeaderboard(r.Context(), trackID, year, limit, offset, driverID) if err != nil { writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } items := make([]transport.LeaderboardEntry, 0, len(res.Items)) for _, item := range res.Items { items = append(items, leaderboardEntryToWire(item)) } var curDriver *transport.LeaderboardEntry if res.CurrentDriver != nil { wire := leaderboardEntryToWire(*res.CurrentDriver) curDriver = &wire } writeJSON(w, http.StatusOK, transport.LeaderboardResponse{ Items: items, Total: res.Total, Limit: limit, Offset: offset, CurrentDriver: curDriver, }) } } func leaderboardEntryToWire(e races.LeaderboardEntry) transport.LeaderboardEntry { var bestPos *int if e.BestPos != nil { pos := *e.BestPos bestPos = &pos } var bestTime *int64 if e.BestTimeMs != nil { val := *e.BestTimeMs bestTime = &val } return transport.LeaderboardEntry{ DriverID: e.DriverID, Nickname: e.Nickname, Name: e.Name, AvatarURL: e.AvatarURL, ClanID: e.ClanID, ClanTag: e.ClanTag, Points: e.Points, BestPos: bestPos, BestTimeMs: bestTime, Rank: e.Rank, } }