import { Prisma } from "@/generated/prisma/client"; import { apiJson } from "@/lib/api"; import { bearerUserId } from "@/lib/api-auth"; import { auth } from "@/lib/auth"; import { logger } from "@/lib/logger"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; type BadgeRarityKey = | "common" | "rare" | "epic" | "legendary" | "mythical" | "unique"; interface BadgeLeaderboardEntry { userId: number; username: string; figure: string; score: number; rank: number; } interface BadgeLeaderboardBoard { entries: BadgeLeaderboardEntry[]; totalPlayers: number; viewerEntry?: Partial; } interface BadgeLeaderboardStat { badgeCode: string; ownerCount: number; rarity: BadgeRarityKey; } const THRESHOLDS = { commonMinOwners: 500, rareMinOwners: 100, epicMinOwners: 20, legendaryMinOwners: 5, mythicalMinOwners: 2, uniqueOwners: 1, }; function assignRarity(ownerCount: number): BadgeRarityKey { if (ownerCount >= THRESHOLDS.commonMinOwners) return "common"; if (ownerCount >= THRESHOLDS.rareMinOwners) return "rare"; if (ownerCount >= THRESHOLDS.epicMinOwners) return "epic"; if (ownerCount >= THRESHOLDS.legendaryMinOwners) return "legendary"; if (ownerCount >= THRESHOLDS.mythicalMinOwners) return "mythical"; return "unique"; } function rankEntries( entries: BadgeLeaderboardEntry[], ): BadgeLeaderboardEntry[] { return entries .sort((a, b) => b.score - a.score) .map((e, i) => ({ ...e, rank: i + 1 })); } async function loadTotalBadgesBoard( userId: number | null, ): Promise { const rows = await prisma.$queryRaw< Array<{ userId: bigint; username: string; look: string; cnt: bigint }> >( Prisma.sql` SELECT ub.user_id AS userId, u.username, u.look, COUNT(*) AS cnt FROM users_badges ub JOIN users u ON u.id = ub.user_id GROUP BY ub.user_id ORDER BY cnt DESC LIMIT 20 `, ); const entries = rankEntries( rows.map((r) => ({ userId: Number(r.userId), username: r.username, figure: r.look, score: Number(r.cnt), rank: 0, })), ); const totalPlayers = entries.length > 0 ? Number( ( await prisma.$queryRaw>( Prisma.sql`SELECT COUNT(DISTINCT user_id) AS cnt FROM users_badges`, ) )[0]?.cnt ?? 0, ) : 0; let viewerEntry: Partial | undefined; if (userId) { const viewerRow = await prisma.$queryRaw>( Prisma.sql`SELECT COUNT(*) AS cnt FROM users_badges WHERE user_id = ${userId}`, ); const viewerScore = Number(viewerRow[0]?.cnt ?? 0); if (viewerScore > 0) { const viewerRank = entries.length > 0 ? Number( ( await prisma.$queryRaw>( Prisma.sql` SELECT COUNT(*) + 1 AS cnt FROM (SELECT user_id, COUNT(*) AS total FROM users_badges GROUP BY user_id) t WHERE t.total > ${viewerScore} `, ) )[0]?.cnt ?? entries.length + 1, ) : 1; const viewerUser = await prisma.user.findUnique({ where: { id: userId }, select: { username: true, look: true }, }); if (viewerUser) { viewerEntry = { userId, username: viewerUser.username, figure: viewerUser.look, score: viewerScore, rank: viewerRank, }; } } } return { entries, totalPlayers, viewerEntry }; } async function loadAchievementBoard( userId: number | null, ): Promise { const rows = await prisma.$queryRaw< Array<{ userId: bigint; username: string; look: string; score: number }> >( Prisma.sql` SELECT us.user_id AS userId, u.username, u.look, us.achievement_score AS score FROM users_settings us JOIN users u ON u.id = us.user_id ORDER BY us.achievement_score DESC LIMIT 20 `, ); const entries = rankEntries( rows.map((r) => ({ userId: Number(r.userId), username: r.username, figure: r.look, score: r.score, rank: 0, })), ); const totalPlayers = entries.length > 0 ? Number( ( await prisma.$queryRaw>( Prisma.sql`SELECT COUNT(*) AS cnt FROM users_settings WHERE achievement_score > 0`, ) )[0]?.cnt ?? 0, ) : 0; let viewerEntry: Partial | undefined; if (userId) { const viewerUser = await prisma.user.findUnique({ where: { id: userId }, select: { username: true, look: true }, }); if (viewerUser) { const viewerSettings = await prisma.usersSettings.findUnique({ where: { userId }, select: { achievementScore: true }, }); if (viewerSettings && viewerSettings.achievementScore > 0) { const viewerRank = Number( ( await prisma.$queryRaw>( Prisma.sql` SELECT COUNT(*) + 1 AS cnt FROM users_settings WHERE achievement_score > ${viewerSettings.achievementScore} `, ) )[0]?.cnt ?? entries.length + 1, ); viewerEntry = { userId, username: viewerUser.username, figure: viewerUser.look, score: viewerSettings.achievementScore, rank: viewerRank, }; } } } return { entries, totalPlayers, viewerEntry }; } async function loadRarityBoard( _rarity: BadgeRarityKey, badgeCodes: string[], userId: number | null, ): Promise { if (badgeCodes.length === 0) return { entries: [], totalPlayers: 0 }; const codes = Prisma.join(badgeCodes); const rows = await prisma.$queryRaw< Array<{ userId: bigint; username: string; look: string; cnt: bigint }> >( Prisma.sql` SELECT ub.user_id AS userId, u.username, u.look, COUNT(*) AS cnt FROM users_badges ub JOIN users u ON u.id = ub.user_id WHERE ub.badge_code IN (${codes}) GROUP BY ub.user_id ORDER BY cnt DESC LIMIT 20 `, ); const entries = rankEntries( rows.map((r) => ({ userId: Number(r.userId), username: r.username, figure: r.look, score: Number(r.cnt), rank: 0, })), ); const totalPlayers = rows.length > 0 ? Number( ( await prisma.$queryRaw>( Prisma.sql` SELECT COUNT(DISTINCT user_id) AS cnt FROM users_badges WHERE badge_code IN (${codes}) `, ) )[0]?.cnt ?? 0, ) : 0; let viewerEntry: Partial | undefined; if (userId) { const viewerRow = await prisma.$queryRaw>( Prisma.sql` SELECT COUNT(*) AS cnt FROM users_badges WHERE user_id = ${userId} AND badge_code IN (${codes}) `, ); const viewerScore = Number(viewerRow[0]?.cnt ?? 0); if (viewerScore > 0) { const viewerUser = await prisma.user.findUnique({ where: { id: userId }, select: { username: true, look: true }, }); if (viewerUser) { const viewerRank = Number( ( await prisma.$queryRaw>( Prisma.sql` SELECT COUNT(*) + 1 AS cnt FROM (SELECT user_id, COUNT(*) AS total FROM users_badges WHERE badge_code IN (${codes}) GROUP BY user_id) t WHERE t.total > ${viewerScore} `, ) )[0]?.cnt ?? entries.length + 1, ); viewerEntry = { userId, username: viewerUser.username, figure: viewerUser.look, score: viewerScore, rank: viewerRank, }; } } } return { entries, totalPlayers, viewerEntry }; } export async function GET(req: Request) { try { let userId: number | null = await bearerUserId(req); if (!userId) { const session = await auth(); userId = session?.user?.id ? Number(session.user.id) : null; } const badgeStatsRaw = await prisma.$queryRaw< Array<{ badgeCode: string; ownerCount: bigint }> >( Prisma.sql` SELECT badge_code AS badgeCode, COUNT(DISTINCT user_id) AS ownerCount FROM users_badges GROUP BY badge_code `, ); const badgeStats: BadgeLeaderboardStat[] = badgeStatsRaw.map((r) => ({ badgeCode: r.badgeCode, ownerCount: Number(r.ownerCount), rarity: assignRarity(Number(r.ownerCount)), })); const badgesByRarity = new Map(); for (const stat of badgeStats) { const list = badgesByRarity.get(stat.rarity) ?? []; list.push(stat.badgeCode); badgesByRarity.set(stat.rarity, list); } const rarityKeys: BadgeRarityKey[] = [ "common", "rare", "epic", "legendary", "mythical", "unique", ]; const [totalBadges, achievementLevel, ...rarityBoards] = await Promise.all([ loadTotalBadgesBoard(userId), loadAchievementBoard(userId), ...rarityKeys.map((rk) => loadRarityBoard(rk, badgesByRarity.get(rk) ?? [], userId), ), ]); const rarity = Object.fromEntries( // eslint-disable-next-line security/detect-object-injection -- rk from rarityKeys const, i is array index rarityKeys.map((rk, i) => [rk, rarityBoards[i]]), ) as Record; return apiJson({ viewerUserId: userId ?? 0, badgeStats, thresholds: THRESHOLDS, leaderboards: { totalBadges, achievementLevel, rarity }, }); } catch (err) { logger.error("Badge leaderboard error", { module: "badges/leaderboard", error: String(err), }); return apiJson({ viewerUserId: 0, badgeStats: [], thresholds: THRESHOLDS, leaderboards: { totalBadges: { entries: [], totalPlayers: 0 }, achievementLevel: { entries: [], totalPlayers: 0 }, rarity: { common: { entries: [], totalPlayers: 0 }, rare: { entries: [], totalPlayers: 0 }, epic: { entries: [], totalPlayers: 0 }, legendary: { entries: [], totalPlayers: 0 }, mythical: { entries: [], totalPlayers: 0 }, unique: { entries: [], totalPlayers: 0 }, }, }, }); } }