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, } } // calendarLeaderboardHandler godoc // @Summary Track Calendar Leaderboards // @Description Returns the list of scheduled tracks with their status (finished/active/planned), top 3 drivers, and current driver position, sorted by calendar start date. // @Tags leaderboard // @Produce json // @Param year query int false "Year. Defaults to current year." // @Param X-Driver-Id header string false "Current driver ID. Defaults to driver-seed-001 (mock Alice)." // @Success 200 {object} transport.TrackCalendarLeaderboardResponse "Calendar leaderboards list" // @Failure 400 {object} map[string]interface{} "Invalid input" // @Failure 500 {object} map[string]interface{} "Internal server error" // @Router /api/leaderboard/tracks [get] func calendarLeaderboardHandler(svc *races.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { 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 } } driverID := r.Header.Get("X-Driver-Id") if driverID == "" { driverID = "driver-seed-001" // mock current driver } res, err := svc.GetCalendarLeaderboard(r.Context(), year, driverID) if err != nil { writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } items := make([]transport.TrackCalendarLeaderboard, 0, len(res)) for _, item := range res { podium := make([]transport.LeaderboardEntry, 0, len(item.Podium)) for _, entry := range item.Podium { podium = append(podium, leaderboardEntryToWire(entry)) } var curDriver *transport.LeaderboardEntry if item.CurrentDriver != nil { wire := leaderboardEntryToWire(*item.CurrentDriver) curDriver = &wire } items = append(items, transport.TrackCalendarLeaderboard{ TrackID: item.TrackID, TrackName: item.TrackName, StartAt: item.StartAt, EndAt: item.EndAt, Status: item.Status, Podium: podium, CurrentDriver: curDriver, }) } writeJSON(w, http.StatusOK, transport.TrackCalendarLeaderboardResponse{Items: items}) } }