import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { db } from '@/lib/db'; interface RouteContext { params: Promise<{ id: string }>; } interface Team { id: string; clientId: string; partnerId: string | null; teamName: string | null; seedNumber: number | null; client: { id: string; firstName: string; lastName: string; }; } interface MatchData { tournamentId: string; round: number; position: number; status: 'SCHEDULED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED' | 'WALKOVER'; team1Players: string[]; team2Players: string[]; } // Calculate next power of 2 that is >= n function nextPowerOf2(n: number): number { let power = 1; while (power < n) { power *= 2; } return power; } // Calculate number of rounds for single elimination function calculateRounds(teamCount: number): number { return Math.ceil(Math.log2(teamCount)); } // Generate single elimination bracket function generateSingleEliminationBracket( tournamentId: string, teams: Team[] ): MatchData[] { const teamCount = teams.length; const bracketSize = nextPowerOf2(teamCount); const totalRounds = calculateRounds(bracketSize); const byeCount = bracketSize - teamCount; const matches: MatchData[] = []; // Sort teams by seed number (if available) for proper seeding const sortedTeams = [...teams].sort((a, b) => { if (a.seedNumber === null && b.seedNumber === null) return 0; if (a.seedNumber === null) return 1; if (b.seedNumber === null) return -1; return a.seedNumber - b.seedNumber; }); // Create seeding positions for standard bracket seeding // For 8 teams: [1,8,4,5,2,7,3,6] - this ensures top seeds meet later const seedingOrder = createSeedingOrder(bracketSize); // Map teams to their seeding positions const teamSlots: (Team | null)[] = new Array(bracketSize).fill(null); for (let i = 0; i < sortedTeams.length; i++) { const position = seedingOrder[i] - 1; // Convert to 0-indexed teamSlots[position] = sortedTeams[i]; } // First round matches (round 1) const firstRoundMatchCount = bracketSize / 2; for (let pos = 0; pos < firstRoundMatchCount; pos++) { const team1Index = pos * 2; const team2Index = pos * 2 + 1; const team1 = teamSlots[team1Index]; const team2 = teamSlots[team2Index]; const match: MatchData = { tournamentId, round: 1, position: pos + 1, status: 'SCHEDULED', team1Players: team1 ? getTeamPlayerIds(team1) : [], team2Players: team2 ? getTeamPlayerIds(team2) : [], }; // If one team is a bye (null), mark as walkover and advance the other team if (!team1 && team2) { match.status = 'WALKOVER'; match.team1Players = []; // Bye } else if (team1 && !team2) { match.status = 'WALKOVER'; match.team2Players = []; // Bye } else if (!team1 && !team2) { // Both are byes - this shouldn't happen with proper seeding match.status = 'WALKOVER'; } matches.push(match); } // Create subsequent round matches (empty, to be filled as tournament progresses) for (let round = 2; round <= totalRounds; round++) { const matchesInRound = bracketSize / Math.pow(2, round); for (let pos = 0; pos < matchesInRound; pos++) { matches.push({ tournamentId, round, position: pos + 1, status: 'SCHEDULED', team1Players: [], team2Players: [], }); } } return matches; } // Get player IDs for a team (client + partner if exists) function getTeamPlayerIds(team: Team): string[] { const ids = [team.clientId]; if (team.partnerId) { ids.push(team.partnerId); } return ids; } // Create standard bracket seeding order // For power of 2 brackets, ensures that #1 seed meets #2 seed only in finals function createSeedingOrder(size: number): number[] { if (size === 2) return [1, 2]; const order: number[] = []; const halfSize = size / 2; const prevOrder = createSeedingOrder(halfSize); for (let i = 0; i < halfSize; i++) { order.push(prevOrder[i]); order.push(size + 1 - prevOrder[i]); } return order; } // Process walkover matches and advance winners async function processWalkovers( tx: Omit, matches: { id: string; round: number; position: number; status: string; team1Players: string[]; team2Players: string[] }[] ): Promise { const walkovers = matches.filter(m => m.status === 'WALKOVER'); for (const match of walkovers) { // Determine winner (the team that's not empty) const winner = match.team1Players.length > 0 ? match.team1Players : match.team2Players; if (winner.length === 0) continue; // Both empty, no winner to advance // Find the next round match this winner advances to const nextRound = match.round + 1; const nextPosition = Math.ceil(match.position / 2); const nextMatch = matches.find(m => m.round === nextRound && m.position === nextPosition); if (!nextMatch) continue; // Final match or not found // Determine if this match feeds into team1 or team2 of next match const isUpperBracket = match.position % 2 === 1; if (isUpperBracket) { await tx.match.update({ where: { id: nextMatch.id }, data: { team1Players: winner }, }); } else { await tx.match.update({ where: { id: nextMatch.id }, data: { team2Players: winner }, }); } // Update the walkover match with winnerId await tx.match.update({ where: { id: match.id }, data: { winnerId: winner[0], // Use first player as winner identifier completedAt: new Date(), status: 'COMPLETED', }, }); } } // POST /api/tournaments/[id]/generate-bracket - Generate bracket/matches export async function POST( request: NextRequest, context: RouteContext ) { try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } // Check if user has admin role const allowedRoles = ['SUPER_ADMIN', 'ORG_ADMIN', 'SITE_ADMIN']; if (!allowedRoles.includes(session.user.role)) { return NextResponse.json( { error: 'Forbidden: Insufficient permissions' }, { status: 403 } ); } const { id } = await context.params; // Verify tournament exists and belongs to user's organization const tournament = await db.tournament.findFirst({ where: { id, organizationId: session.user.organizationId, }, include: { inscriptions: { include: { client: { select: { id: true, firstName: true, lastName: true, }, }, }, }, matches: true, }, }); if (!tournament) { return NextResponse.json( { error: 'Tournament not found' }, { status: 404 } ); } // Check tournament status is REGISTRATION_OPEN if (tournament.status !== 'REGISTRATION_OPEN') { return NextResponse.json( { error: 'Tournament must be in REGISTRATION_OPEN status to generate bracket. Current status: ' + tournament.status }, { status: 400 } ); } // Check minimum inscriptions (at least 2 teams) if (tournament.inscriptions.length < 2) { return NextResponse.json( { error: 'At least 2 teams must be registered to generate bracket' }, { status: 400 } ); } // Check if bracket already exists if (tournament.matches.length > 0) { return NextResponse.json( { error: 'Bracket already exists. Delete existing matches first if you want to regenerate.' }, { status: 400 } ); } // Get tournament format from settings const settings = tournament.settings as { tournamentFormat?: string } | null; const format = settings?.tournamentFormat || 'SINGLE_ELIMINATION'; // Currently only supporting single elimination if (format !== 'SINGLE_ELIMINATION' && format !== 'BRACKET') { return NextResponse.json( { error: `Bracket generation for ${format} format is not yet supported. Only SINGLE_ELIMINATION is currently available.` }, { status: 400 } ); } // Generate bracket matches const matchData = generateSingleEliminationBracket( id, tournament.inscriptions as Team[] ); // Create matches in transaction const result = await db.$transaction(async (tx) => { // Create all matches const createdMatches = []; for (const match of matchData) { const created = await tx.match.create({ data: match, }); createdMatches.push({ ...created, status: match.status, }); } // Process walkovers and advance winners await processWalkovers(tx, createdMatches as { id: string; round: number; position: number; status: string; team1Players: string[]; team2Players: string[] }[]); // Update tournament status to IN_PROGRESS await tx.tournament.update({ where: { id }, data: { status: 'IN_PROGRESS' }, }); // Fetch updated matches return tx.match.findMany({ where: { tournamentId: id }, orderBy: [ { round: 'asc' }, { position: 'asc' }, ], }); }); return NextResponse.json({ message: 'Bracket generated successfully', tournamentStatus: 'IN_PROGRESS', matches: result, totalMatches: result.length, totalRounds: Math.max(...result.map(m => m.round)), }); } catch (error) { console.error('Error generating bracket:', error); return NextResponse.json( { error: 'Failed to generate bracket' }, { status: 500 } ); } }