import { and, asc, desc, eq, inArray, sql } from "drizzle-orm"; import { matchParticipants, matchScoreEvents, matchSessions, users, type InsertMatchParticipant, type InsertMatchScoreEvent, type InsertMatchSession, } from "../drizzle/schema"; import { createNotification, getDateKey, getDb, refreshAchievementsForUser, refreshUserTrainingSummary, upsertDailyTrainingAggregate, upsertTrainingRecordBySource, } from "./db"; import { MATCH_PLAYER_SLOTS, buildParticipantSettlement, deriveSuggestedMatchState, normalizeMatchMetrics, normalizeMatchScoreboard, type MatchPlayerSlot, } from "./match"; type MatchMode = "daily" | "competitive"; type WorkflowStatus = "draft" | "recording" | "review_pending" | "reviewed" | "finalizing" | "finalized" | "cancelled"; type SuggestionStatus = "idle" | "queued" | "ready" | "failed"; type CameraStatus = "pending" | "bound" | "active" | "completed" | "failed"; type EventSource = "camera_a" | "camera_b" | "system" | "admin"; type EventType = "point" | "game" | "set" | "metric" | "score_suggestion" | "review_adjustment" | "finalized"; type ParticipantRow = Awaited>[number]; function orderByIds(rows: T[], ids: number[]) { const order = new Map(ids.map((id, index) => [id, index])); return [...rows].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)); } function indexBySlot(rows: ParticipantRow[]) { return { player_a: rows.find((row) => row.playerSlot === "player_a") ?? null, player_b: rows.find((row) => row.playerSlot === "player_b") ?? null, }; } async function loadParticipantsMap(matchIds: number[]) { if (matchIds.length === 0) { return new Map(); } const db = await getDb(); if (!db) return new Map(); const rows = await db.select({ id: matchParticipants.id, matchId: matchParticipants.matchId, userId: matchParticipants.userId, userName: users.name, playerSlot: matchParticipants.playerSlot, cameraSlot: matchParticipants.cameraSlot, cameraStatus: matchParticipants.cameraStatus, cameraLabel: matchParticipants.cameraLabel, cameraVideoId: matchParticipants.cameraVideoId, cameraVideoUrl: matchParticipants.cameraVideoUrl, cameraSnapshot: matchParticipants.cameraSnapshot, isWinner: matchParticipants.isWinner, suggestedSetsWon: matchParticipants.suggestedSetsWon, suggestedGamesWon: matchParticipants.suggestedGamesWon, suggestedPointsWon: matchParticipants.suggestedPointsWon, finalSetsWon: matchParticipants.finalSetsWon, finalGamesWon: matchParticipants.finalGamesWon, finalPointsWon: matchParticipants.finalPointsWon, suggestedStats: matchParticipants.suggestedStats, finalStats: matchParticipants.finalStats, createdAt: matchParticipants.createdAt, updatedAt: matchParticipants.updatedAt, }).from(matchParticipants) .leftJoin(users, eq(users.id, matchParticipants.userId)) .where(inArray(matchParticipants.matchId, matchIds)) .orderBy(asc(matchParticipants.matchId), asc(matchParticipants.id)); const grouped = new Map(); for (const row of rows) { const list = grouped.get(row.matchId) ?? []; list.push(row); grouped.set(row.matchId, list); } return grouped; } async function loadEventCounts(matchIds: number[]) { if (matchIds.length === 0) { return new Map(); } const db = await getDb(); if (!db) return new Map(); const rows = await db.select({ matchId: matchScoreEvents.matchId, }).from(matchScoreEvents).where(inArray(matchScoreEvents.matchId, matchIds)); const counts = new Map(); for (const row of rows) { counts.set(row.matchId, (counts.get(row.matchId) ?? 0) + 1); } return counts; } async function hydrateMatches(sessionIds: number[]) { const db = await getDb(); if (!db || sessionIds.length === 0) return []; const sessions = await db.select().from(matchSessions) .where(inArray(matchSessions.id, sessionIds)); const participantsMap = await loadParticipantsMap(sessionIds); const eventCounts = await loadEventCounts(sessionIds); return orderByIds(sessions, sessionIds).map((session) => ({ ...session, participants: participantsMap.get(session.id) ?? [], eventCount: eventCounts.get(session.id) ?? 0, })); } export async function getMatchSessionById(matchId: number) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(matchSessions).where(eq(matchSessions.id, matchId)).limit(1); return result[0]; } export async function getMatchParticipantBySlot(matchId: number, playerSlot: MatchPlayerSlot) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(matchParticipants) .where(and(eq(matchParticipants.matchId, matchId), eq(matchParticipants.playerSlot, playerSlot))) .limit(1); return result[0]; } export async function listMatchParticipants(matchId: number) { const db = await getDb(); if (!db) return []; return db.select({ id: matchParticipants.id, matchId: matchParticipants.matchId, userId: matchParticipants.userId, userName: users.name, playerSlot: matchParticipants.playerSlot, cameraSlot: matchParticipants.cameraSlot, cameraStatus: matchParticipants.cameraStatus, cameraLabel: matchParticipants.cameraLabel, cameraVideoId: matchParticipants.cameraVideoId, cameraVideoUrl: matchParticipants.cameraVideoUrl, cameraSnapshot: matchParticipants.cameraSnapshot, isWinner: matchParticipants.isWinner, suggestedSetsWon: matchParticipants.suggestedSetsWon, suggestedGamesWon: matchParticipants.suggestedGamesWon, suggestedPointsWon: matchParticipants.suggestedPointsWon, finalSetsWon: matchParticipants.finalSetsWon, finalGamesWon: matchParticipants.finalGamesWon, finalPointsWon: matchParticipants.finalPointsWon, suggestedStats: matchParticipants.suggestedStats, finalStats: matchParticipants.finalStats, createdAt: matchParticipants.createdAt, updatedAt: matchParticipants.updatedAt, }).from(matchParticipants) .leftJoin(users, eq(users.id, matchParticipants.userId)) .where(eq(matchParticipants.matchId, matchId)) .orderBy(asc(matchParticipants.id)); } export async function listMatchScoreEvents(matchId: number) { const db = await getDb(); if (!db) return []; return db.select().from(matchScoreEvents) .where(eq(matchScoreEvents.matchId, matchId)) .orderBy(asc(matchScoreEvents.eventIndex), asc(matchScoreEvents.id)); } export async function getMatchDetail(matchId: number) { const session = await getMatchSessionById(matchId); if (!session) return undefined; const participants = await listMatchParticipants(matchId); const events = await listMatchScoreEvents(matchId); return { ...session, participants, events, eventCount: events.length, }; } export async function listAccessibleMatchSessions(params: { viewerUserId: number; isAdmin: boolean; limit?: number; workflowStatus?: WorkflowStatus | "all"; matchMode?: MatchMode | "all"; }) { const db = await getDb(); if (!db) return []; const limit = params.limit ?? 50; const filters = []; if (params.workflowStatus && params.workflowStatus !== "all") { filters.push(eq(matchSessions.workflowStatus, params.workflowStatus)); } if (params.matchMode && params.matchMode !== "all") { filters.push(eq(matchSessions.matchMode, params.matchMode)); } if (params.isAdmin) { const sessions = await db.select({ id: matchSessions.id, }).from(matchSessions) .where(filters.length > 0 ? and(...filters) : undefined) .orderBy(desc(matchSessions.updatedAt), desc(matchSessions.id)) .limit(limit); return hydrateMatches(sessions.map((row) => row.id)); } const conditions = [ eq(matchParticipants.userId, params.viewerUserId), ...filters, ]; const sessions = await db.select({ id: matchSessions.id, }).from(matchParticipants) .innerJoin(matchSessions, eq(matchSessions.id, matchParticipants.matchId)) .where(and(...conditions)) .orderBy(desc(matchSessions.updatedAt), desc(matchSessions.id)) .limit(limit); return hydrateMatches(sessions.map((row) => row.id)); } export async function createMatchSession(input: { createdByUserId: number; matchMode: MatchMode; title: string; courtName?: string | null; notes?: string | null; durationMinutes: number; scheduledAt?: Date | null; participantUserIds: [number, number]; }) { if (input.participantUserIds[0] === input.participantUserIds[1]) { throw new Error("两位参赛用户必须不同"); } const db = await getDb(); if (!db) throw new Error("Database not available"); const matchId = await db.transaction(async (tx) => { const sessionValues: InsertMatchSession = { createdByUserId: input.createdByUserId, matchMode: input.matchMode, workflowStatus: "draft", title: input.title, courtName: input.courtName ?? null, notes: input.notes ?? null, durationMinutes: input.durationMinutes, scheduledAt: input.scheduledAt ?? null, suggestionStatus: "idle", suggestionTaskId: null, suggestedScore: null, suggestedMetrics: null, finalScore: null, finalMetrics: null, reviewNotes: null, reviewSubmittedAt: null, reviewedByUserId: null, reviewedAt: null, finalizedByUserId: null, finalizedAt: null, }; const inserted = await tx.insert(matchSessions).values(sessionValues); const createdMatchId = inserted[0].insertId; const participantValues: InsertMatchParticipant[] = [ { matchId: createdMatchId, userId: input.participantUserIds[0], playerSlot: "player_a", cameraSlot: "camera_a", cameraStatus: "pending", cameraLabel: null, cameraVideoId: null, cameraVideoUrl: null, cameraSnapshot: null, isWinner: 0, suggestedSetsWon: 0, suggestedGamesWon: 0, suggestedPointsWon: 0, finalSetsWon: 0, finalGamesWon: 0, finalPointsWon: 0, suggestedStats: null, finalStats: null, }, { matchId: createdMatchId, userId: input.participantUserIds[1], playerSlot: "player_b", cameraSlot: "camera_b", cameraStatus: "pending", cameraLabel: null, cameraVideoId: null, cameraVideoUrl: null, cameraSnapshot: null, isWinner: 0, suggestedSetsWon: 0, suggestedGamesWon: 0, suggestedPointsWon: 0, finalSetsWon: 0, finalGamesWon: 0, finalPointsWon: 0, suggestedStats: null, finalStats: null, }, ]; await tx.insert(matchParticipants).values(participantValues); return createdMatchId; }); return getMatchDetail(matchId); } export async function updateMatchSession(matchId: number, patch: Partial) { const db = await getDb(); if (!db) return; await db.update(matchSessions).set(patch).where(eq(matchSessions.id, matchId)); } export async function bindMatchCamera(input: { matchId: number; playerSlot: MatchPlayerSlot; cameraStatus: CameraStatus; cameraLabel?: string | null; cameraVideoId?: number | null; cameraVideoUrl?: string | null; cameraSnapshot?: unknown; }) { const db = await getDb(); if (!db) throw new Error("Database not available"); const participant = await getMatchParticipantBySlot(input.matchId, input.playerSlot); if (!participant) { throw new Error("Match participant not found"); } await db.update(matchParticipants).set({ cameraStatus: input.cameraStatus, cameraLabel: input.cameraLabel === undefined ? participant.cameraLabel : input.cameraLabel, cameraVideoId: input.cameraVideoId === undefined ? participant.cameraVideoId : input.cameraVideoId, cameraVideoUrl: input.cameraVideoUrl === undefined ? participant.cameraVideoUrl : input.cameraVideoUrl, cameraSnapshot: input.cameraSnapshot === undefined ? participant.cameraSnapshot : input.cameraSnapshot, }).where(eq(matchParticipants.id, participant.id)); const session = await getMatchSessionById(input.matchId); if (session?.workflowStatus === "draft" && input.cameraStatus !== "pending") { await updateMatchSession(input.matchId, { workflowStatus: "recording", }); } return getMatchDetail(input.matchId); } async function getNextEventIndex(matchId: number) { const db = await getDb(); if (!db) return 1; const result = await db.select({ maxEventIndex: sql`coalesce(max(${matchScoreEvents.eventIndex}), 0)`, }).from(matchScoreEvents).where(eq(matchScoreEvents.matchId, matchId)); return (result[0]?.maxEventIndex ?? 0) + 1; } export async function insertMatchScoreEvent(input: { matchId: number; source: EventSource; eventType: EventType; winnerSlot?: MatchPlayerSlot | null; matchSecond?: number | null; confidence?: number | null; payload?: unknown; createdByUserId?: number | null; }) { const db = await getDb(); if (!db) throw new Error("Database not available"); const eventIndex = await getNextEventIndex(input.matchId); const values: InsertMatchScoreEvent = { matchId: input.matchId, eventIndex, source: input.source, eventType: input.eventType, winnerSlot: input.winnerSlot ?? null, matchSecond: input.matchSecond ?? null, confidence: input.confidence ?? null, payload: input.payload ?? null, createdByUserId: input.createdByUserId ?? null, }; const inserted = await db.insert(matchScoreEvents).values(values); const rows = await db.select().from(matchScoreEvents).where(eq(matchScoreEvents.id, inserted[0].insertId)).limit(1); const session = await getMatchSessionById(input.matchId); if (session && session.workflowStatus === "draft") { await updateMatchSession(input.matchId, { workflowStatus: "recording", }); } return rows[0]; } export async function saveMatchSuggestion(matchId: number, params: { suggestionStatus: SuggestionStatus; suggestionTaskId?: string | null; suggestedScore?: unknown; suggestedMetrics?: unknown; }) { const session = await getMatchSessionById(matchId); if (!session) { throw new Error("Match session not found"); } const score = params.suggestedScore ? normalizeMatchScoreboard(params.suggestedScore) : normalizeMatchScoreboard(session.suggestedScore); const metrics = params.suggestedMetrics ? normalizeMatchMetrics(params.suggestedMetrics) : normalizeMatchMetrics(session.suggestedMetrics); const settlement = buildParticipantSettlement(score, metrics); const participants = await listMatchParticipants(matchId); await updateMatchSession(matchId, { suggestionStatus: params.suggestionStatus, suggestionTaskId: params.suggestionTaskId === undefined ? session.suggestionTaskId : params.suggestionTaskId, suggestedScore: params.suggestedScore === undefined ? session.suggestedScore : score, suggestedMetrics: params.suggestedMetrics === undefined ? session.suggestedMetrics : metrics, workflowStatus: session.workflowStatus === "draft" || session.workflowStatus === "recording" ? (params.suggestionStatus === "ready" ? "review_pending" : session.workflowStatus) : session.workflowStatus, }); const rowsBySlot = indexBySlot(participants); for (const slot of MATCH_PLAYER_SLOTS) { const row = rowsBySlot[slot]; if (!row) continue; const player = settlement.players[slot]; const dbConn = await getDb(); if (!dbConn) continue; await dbConn.update(matchParticipants).set({ suggestedSetsWon: player.setsWon, suggestedGamesWon: player.gamesWon, suggestedPointsWon: player.pointsWon, suggestedStats: player.stats, }).where(eq(matchParticipants.id, row.id)); } return getMatchDetail(matchId); } export async function submitMatchReview(input: { matchId: number; reviewedByUserId: number; reviewNotes?: string | null; finalScore?: unknown; finalMetrics?: unknown; }) { const session = await getMatchSessionById(input.matchId); if (!session) { throw new Error("Match session not found"); } const score = normalizeMatchScoreboard(input.finalScore ?? session.suggestedScore); const metrics = normalizeMatchMetrics(input.finalMetrics ?? session.suggestedMetrics); const settlement = buildParticipantSettlement(score, metrics); const participants = await listMatchParticipants(input.matchId); const db = await getDb(); if (!db) throw new Error("Database not available"); await updateMatchSession(input.matchId, { finalScore: score, finalMetrics: metrics, reviewNotes: input.reviewNotes ?? session.reviewNotes, reviewSubmittedAt: new Date(), reviewedByUserId: input.reviewedByUserId, reviewedAt: new Date(), workflowStatus: "reviewed", }); const rowsBySlot = indexBySlot(participants); for (const slot of MATCH_PLAYER_SLOTS) { const row = rowsBySlot[slot]; if (!row) continue; const player = settlement.players[slot]; await db.update(matchParticipants).set({ isWinner: player.isWinner ? 1 : 0, finalSetsWon: player.setsWon, finalGamesWon: player.gamesWon, finalPointsWon: player.pointsWon, finalStats: player.stats, }).where(eq(matchParticipants.id, row.id)); } await insertMatchScoreEvent({ matchId: input.matchId, source: "admin", eventType: "review_adjustment", winnerSlot: settlement.winnerSlot, confidence: 1, payload: { score, metrics, reviewNotes: input.reviewNotes ?? null, }, createdByUserId: input.reviewedByUserId, }); return getMatchDetail(input.matchId); } export async function markMatchSuggestionQueued(matchId: number, taskId: string) { const session = await getMatchSessionById(matchId); if (!session) { throw new Error("Match session not found"); } await updateMatchSession(matchId, { suggestionStatus: "queued", suggestionTaskId: taskId, workflowStatus: session.workflowStatus === "draft" ? "recording" : session.workflowStatus, }); } export async function markMatchFinalizing(matchId: number) { const session = await getMatchSessionById(matchId); if (!session) { throw new Error("Match session not found"); } await updateMatchSession(matchId, { workflowStatus: "finalizing", }); } export async function cancelMatchSession(matchId: number, notes?: string | null) { const session = await getMatchSessionById(matchId); if (!session) { throw new Error("Match session not found"); } await updateMatchSession(matchId, { workflowStatus: "cancelled", notes: notes === undefined ? session.notes : notes, }); return getMatchDetail(matchId); } export async function generateSuggestedMatchState(matchId: number) { const detail = await getMatchDetail(matchId); if (!detail) { throw new Error("Match session not found"); } const suggestion = deriveSuggestedMatchState(detail.events.map((event) => ({ eventType: event.eventType, winnerSlot: event.winnerSlot, confidence: event.confidence, payload: event.payload, source: event.source, eventIndex: event.eventIndex, }))); await saveMatchSuggestion(matchId, { suggestionStatus: "ready", suggestionTaskId: detail.suggestionTaskId, suggestedScore: suggestion.score, suggestedMetrics: suggestion.metrics, }); await insertMatchScoreEvent({ matchId, source: "system", eventType: "score_suggestion", winnerSlot: suggestion.score.winnerSlot, confidence: suggestion.score.confidence, payload: { score: suggestion.score, metrics: suggestion.metrics, eventCount: suggestion.eventCount, sourceCount: suggestion.sourceCount, }, }); return { score: suggestion.score, metrics: suggestion.metrics, eventCount: suggestion.eventCount, sourceCount: suggestion.sourceCount, }; } export async function finalizeMatchSettlement(matchId: number, finalizedByUserId: number) { const detail = await getMatchDetail(matchId); if (!detail) { throw new Error("Match session not found"); } if (detail.workflowStatus === "finalized") { return detail; } if (detail.workflowStatus === "cancelled") { throw new Error("已取消的比赛不能结算"); } const score = normalizeMatchScoreboard(detail.finalScore ?? detail.suggestedScore); const metrics = normalizeMatchMetrics(detail.finalMetrics ?? detail.suggestedMetrics); const settlement = buildParticipantSettlement(score, metrics); const occurredAt = detail.endedAt ?? detail.reviewSubmittedAt ?? detail.startedAt ?? detail.scheduledAt ?? new Date(); const trainingDateKey = getDateKey(occurredAt); const durationMinutes = Math.max(1, detail.durationMinutes || 90); const participantsBySlot = indexBySlot(detail.participants); const db = await getDb(); if (!db) { throw new Error("Database not available"); } await updateMatchSession(matchId, { finalScore: score, finalMetrics: metrics, workflowStatus: "finalizing", reviewedByUserId: detail.reviewedByUserId ?? finalizedByUserId, reviewedAt: detail.reviewedAt ?? new Date(), reviewSubmittedAt: detail.reviewSubmittedAt ?? new Date(), }); for (const slot of MATCH_PLAYER_SLOTS) { const participant = participantsBySlot[slot]; if (!participant) continue; const opponent = MATCH_PLAYER_SLOTS .filter((item) => item !== slot) .map((item) => participantsBySlot[item]) .find(Boolean) ?? null; const player = settlement.players[slot]; const matchLabel = detail.matchMode === "competitive" ? "竞赛双人比赛" : "日常双人比赛"; const sourceType = detail.matchMode === "competitive" ? "match_competitive" : "match_daily"; const resultLabel = settlement.winnerSlot == null ? "已确认" : player.isWinner ? "获胜" : "失利"; await db.update(matchParticipants).set({ isWinner: player.isWinner ? 1 : 0, finalSetsWon: player.setsWon, finalGamesWon: player.gamesWon, finalPointsWon: player.pointsWon, finalStats: player.stats, cameraStatus: participant.cameraStatus === "active" ? "completed" : participant.cameraStatus, }).where(eq(matchParticipants.id, participant.id)); const upsertResult = await upsertTrainingRecordBySource({ userId: participant.userId, planId: null, linkedPlanId: null, matchConfidence: detail.suggestionStatus === "ready" ? score.confidence : null, exerciseName: matchLabel, exerciseType: "match", sourceType, sourceId: `${sourceType}:${matchId}:${slot}`, videoId: participant.cameraVideoId ?? null, actionCount: player.pointsWon, durationMinutes, completed: 1, notes: `${detail.title} · ${resultLabel}`, poseScore: null, trainingDate: occurredAt, metadata: { matchId, matchMode: detail.matchMode, playerSlot: slot, opponentUserId: opponent?.userId ?? null, winnerSlot: settlement.winnerSlot, finalScore: score, finalMetrics: metrics, courtName: detail.courtName ?? null, cameraStatus: participant.cameraStatus, }, }); if (detail.matchMode === "daily" && upsertResult.isNew) { await upsertDailyTrainingAggregate({ userId: participant.userId, trainingDate: trainingDateKey, deltaMinutes: durationMinutes, deltaSessions: 1, deltaTotalActions: player.pointsWon, deltaEffectiveActions: player.pointsWon, metadata: { latestDailyMatchId: matchId, latestDailyMatchResult: resultLabel, }, }); } if (detail.matchMode === "competitive") { await refreshUserTrainingSummary(participant.userId); } await createNotification({ userId: participant.userId, notificationType: "match_settlement", title: `比赛已入库 · ${detail.title}`.slice(0, 256), message: `${matchLabel} 已完成审核入库,结果:${resultLabel},比分 ${player.setsWon}:${settlement.players[slot === "player_a" ? "player_b" : "player_a"].setsWon} 盘 / ${player.gamesWon}:${settlement.players[slot === "player_a" ? "player_b" : "player_a"].gamesWon} 局。`, isRead: 0, }); await refreshAchievementsForUser(participant.userId); } await insertMatchScoreEvent({ matchId, source: "admin", eventType: "finalized", winnerSlot: settlement.winnerSlot, confidence: 1, payload: { score, metrics, finalizedByUserId, finalizedAt: new Date().toISOString(), }, createdByUserId: finalizedByUserId, }); await updateMatchSession(matchId, { workflowStatus: "finalized", finalizedByUserId, finalizedAt: new Date(), }); return getMatchDetail(matchId); } export async function getAccessibleMatchSummary(params: { viewerUserId: number; isAdmin: boolean; }) { const rows = await listAccessibleMatchSessions({ viewerUserId: params.viewerUserId, isAdmin: params.isAdmin, limit: 200, }); let competitiveWins = 0; let camerasBound = 0; for (const match of rows) { for (const participant of match.participants) { if (participant.cameraStatus !== "pending") { camerasBound += 1; } if (!params.isAdmin && participant.userId !== params.viewerUserId) { continue; } if (match.matchMode === "competitive" && match.workflowStatus === "finalized" && participant.isWinner === 1) { competitiveWins += 1; } } } return { total: rows.length, draft: rows.filter((row) => row.workflowStatus === "draft").length, reviewPending: rows.filter((row) => row.workflowStatus === "review_pending" || row.workflowStatus === "reviewed").length, finalized: rows.filter((row) => row.workflowStatus === "finalized").length, daily: rows.filter((row) => row.matchMode === "daily").length, competitive: rows.filter((row) => row.matchMode === "competitive").length, competitiveWins, camerasBound, }; } export async function getCompetitiveLeaderboard(sortBy: "wins" | "winRate" | "setsWon" | "pointsWon" | "matches" = "wins", limit = 50) { const db = await getDb(); if (!db) return []; const rows = await db.select({ matchId: matchParticipants.matchId, userId: matchParticipants.userId, userName: users.name, skillLevel: users.skillLevel, ntrpRating: users.ntrpRating, isWinner: matchParticipants.isWinner, finalSetsWon: matchParticipants.finalSetsWon, finalGamesWon: matchParticipants.finalGamesWon, finalPointsWon: matchParticipants.finalPointsWon, }).from(matchParticipants) .innerJoin(matchSessions, eq(matchSessions.id, matchParticipants.matchId)) .innerJoin(users, eq(users.id, matchParticipants.userId)) .where(and( eq(matchSessions.matchMode, "competitive"), eq(matchSessions.workflowStatus, "finalized"), )) .orderBy(desc(matchParticipants.matchId), asc(matchParticipants.id)); const matchRows = new Map(); for (const row of rows) { const list = matchRows.get(row.matchId) ?? []; list.push(row); matchRows.set(row.matchId, list); } const leaderboard = new Map(); for (const rowsOfMatch of Array.from(matchRows.values())) { const byUser = rowsOfMatch; for (const row of byUser) { const opponent = byUser.find((item: (typeof rows)[number]) => item.userId !== row.userId) ?? null; const current = leaderboard.get(row.userId) ?? { id: row.userId, name: row.userName ?? null, ntrpRating: row.ntrpRating ?? 1.5, skillLevel: row.skillLevel ?? "beginner", matches: 0, wins: 0, losses: 0, winRate: 0, setsWon: 0, setsLost: 0, gamesWon: 0, gamesLost: 0, pointsWon: 0, pointsLost: 0, }; current.matches += 1; current.wins += row.isWinner === 1 ? 1 : 0; current.losses += row.isWinner === 1 ? 0 : 1; current.setsWon += row.finalSetsWon || 0; current.setsLost += opponent?.finalSetsWon || 0; current.gamesWon += row.finalGamesWon || 0; current.gamesLost += opponent?.finalGamesWon || 0; current.pointsWon += row.finalPointsWon || 0; current.pointsLost += opponent?.finalPointsWon || 0; current.winRate = current.matches > 0 ? Math.round((current.wins / current.matches) * 1000) / 10 : 0; leaderboard.set(row.userId, current); } } const sorted = Array.from(leaderboard.values()).sort((a, b) => { const primary = { wins: [b.wins - a.wins, b.winRate - a.winRate, b.setsWon - a.setsWon, b.pointsWon - a.pointsWon], winRate: [b.winRate - a.winRate, b.wins - a.wins, b.setsWon - a.setsWon, b.pointsWon - a.pointsWon], setsWon: [b.setsWon - a.setsWon, b.wins - a.wins, b.winRate - a.winRate, b.pointsWon - a.pointsWon], pointsWon: [b.pointsWon - a.pointsWon, b.wins - a.wins, b.winRate - a.winRate, b.setsWon - a.setsWon], matches: [b.matches - a.matches, b.wins - a.wins, b.winRate - a.winRate, b.pointsWon - a.pointsWon], }[sortBy]; for (const value of primary) { if (value !== 0) return value; } return a.id - b.id; }); return sorted.slice(0, limit); }