const express = require("express"); const { pool } = require("../db"); const { GAMEMODES, avatarUrl, normalizeGamemode } = require("../config"); const { ValidationError, validateRequest } = require("../middleware/validation"); const { sendError } = require("../utils/http"); const router = express.Router(); const PER_PAGE = 20; const TIER_POINTS_SQL = ` CASE pr.tier WHEN 'HT1' THEN 60 WHEN 'LT1' THEN 45 WHEN 'HT2' THEN 30 WHEN 'LT2' THEN 20 WHEN 'HT3' THEN 10 WHEN 'LT3' THEN 6 WHEN 'HT4' THEN 4 WHEN 'LT4' THEN 3 WHEN 'HT5' THEN 2 WHEN 'LT5' THEN 1 ELSE 0 END`; const GM_POINTS_SQL = ` CASE gm.tier WHEN 'HT1' THEN 60 WHEN 'LT1' THEN 45 WHEN 'HT2' THEN 30 WHEN 'LT2' THEN 20 WHEN 'HT3' THEN 10 WHEN 'LT3' THEN 6 WHEN 'HT4' THEN 4 WHEN 'LT4' THEN 3 WHEN 'HT5' THEN 2 WHEN 'LT5' THEN 1 ELSE 0 END`; const schema = { params: { fields: { gamemode: { type: "string", required: true, minLength: 3, maxLength: 32, toLowerCase: true, }, }, allowUnknown: false, }, query: { fields: { page: { type: "int", min: 1, max: 50000 }, perPage: { type: "int", min: 1, max: 500 }, }, allowUnknown: false, }, }; function validateOrReply(req, res) { try { return validateRequest(req, schema); } catch (error) { if (error instanceof ValidationError) { sendError(res, 400, "VALIDATION_ERROR", error.message, error.details); return null; } sendError(res, 400, "VALIDATION_ERROR", "Invalid request payload."); return null; } } function buildDefaultTiers() { return Object.fromEntries(GAMEMODES.map((mode) => [mode, null])); } router.get("/rankings/:gamemode", async (req, res) => { const validated = validateOrReply(req, res); if (!validated) { return; } try { const gamemode = normalizeGamemode(validated.params.gamemode); const page = validated.query.page || 1; const perPage = validated.query.perPage || PER_PAGE; if (!GAMEMODES.includes(gamemode)) { return sendError(res, 400, "VALIDATION_ERROR", "Invalid gamemode"); } const [countRows] = await pool.query( "SELECT COUNT(*) AS total FROM player_ranks WHERE gamemode = ?", [gamemode] ); const total = Number(countRows[0]?.total || 0); const totalPages = Math.max(Math.ceil(total / perPage), 1); const start = (page - 1) * perPage; if (total === 0) { return res.json({ gamemode, data: [], pagination: { page, perPage, total, totalPages }, }); } const [pageRows] = await pool.query( `SELECT p.id, p.username, p.region, gm.tier AS gm_tier, COALESCE(SUM(${TIER_POINTS_SQL}), 0) AS total_points, ${GM_POINTS_SQL} AS gamemode_points FROM players p JOIN player_ranks gm ON gm.player_id = p.id AND gm.gamemode = ? LEFT JOIN player_ranks pr ON pr.player_id = p.id GROUP BY p.id, p.username, p.region, gm.tier ORDER BY gamemode_points DESC, total_points DESC, p.username ASC LIMIT ? OFFSET ?`, [gamemode, perPage, start] ); const playerIds = pageRows.map((row) => row.id); const [allTierRows] = playerIds.length ? await pool.query( "SELECT player_id, gamemode, tier FROM player_ranks WHERE player_id IN (?)", [playerIds] ) : [[]]; const byId = new Map(); for (let i = 0; i < pageRows.length; i += 1) { const row = pageRows[i]; byId.set(row.id, { id: row.id, username: row.username, region: row.region, avatarUrl: avatarUrl(row.username), tiers: buildDefaultTiers(), totalPoints: Number(row.total_points || 0), gamemodePoints: Number(row.gamemode_points || 0), position: start + i + 1, }); } for (const row of allTierRows) { const target = byId.get(row.player_id); if (!target) { continue; } target.tiers[row.gamemode] = row.tier; } return res.json({ gamemode, data: Array.from(byId.values()), pagination: { page, perPage, total, totalPages, }, }); } catch (error) { console.error("GET /api/rankings/:gamemode failed:", error?.message || error); return sendError(res, 500, "RANKINGS_FETCH_FAILED", "Failed to fetch rankings"); } }); module.exports = router;