import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import { canManageTournament } from "@/lib/permissions"; import { generateRoundRobin, validateScheduleInput, generateVariableRoundRobin, expectedRounds } from "@/lib/schedule-generator"; import { generateTeams, generateTeamsWithRotation, generateRandomTeams, type Player, type Team as TeamPairing } from "@/lib/team-generator"; interface RouteParams { params: Promise<{ id: string; }>; } /** * GET /api/tournaments/[id]/schedule * * Fetch the tournament schedule (rounds with matchups). */ export async function GET(_request: Request, { params }: RouteParams) { try { const { id } = await params; const tournamentId = parseInt(id); if (isNaN(tournamentId)) { return NextResponse.json( { error: "Invalid tournament ID" }, { status: 400 } ); } const permission = await canManageTournament(tournamentId); if (!permission.allowed) { return NextResponse.json( { error: permission.reason || "Not authorized to view this tournament" }, { status: 403 } ); } const tournament = await prisma.event.findUnique({ where: { id: tournamentId }, include: { rounds: { orderBy: { roundNumber: "asc" }, include: { bracketMatchups: { include: { player1P1: true, player1P2: true, player2P1: true, player2P2: true, match: true, }, }, }, }, }, }); if (!tournament) { return NextResponse.json( { error: "Tournament not found" }, { status: 404 } ); } return NextResponse.json({ rounds: tournament.rounds }); } catch (error: unknown) { console.error("Error fetching schedule:", error); const message = error instanceof Error ? error.message : "Failed to fetch schedule"; return NextResponse.json({ error: message }, { status: 500 }); } } /** * POST /api/tournaments/[id]/schedule * * Generate a round-robin schedule for the tournament. * Creates TournamentRound and BracketMatchup records. */ export async function POST(_request: Request, { params }: RouteParams) { try { const { id } = await params; const tournamentId = parseInt(id); if (isNaN(tournamentId)) { return NextResponse.json( { error: "Invalid tournament ID" }, { status: 400 } ); } const permission = await canManageTournament(tournamentId); if (!permission.allowed) { return NextResponse.json( { error: permission.reason || "Not authorized to manage this tournament" }, { status: 403 } ); } // Check tournament exists const tournament = await prisma.event.findUnique({ where: { id: tournamentId }, include: { participants: { include: { player: true, }, }, rounds: true, }, }); if (!tournament) { return NextResponse.json( { error: "Tournament not found" }, { status: 404 } ); } // Check if schedule already exists and delete it if (tournament.rounds.length > 0) { // Delete existing rounds and matchups before regenerating await prisma.bracketMatchup.deleteMany({ where: { eventId: tournamentId }, }); await prisma.tournamentRound.deleteMany({ where: { eventId: tournamentId }, }); } // Get participants as players const participants: Player[] = tournament.participants.map((p) => ({ id: p.player.id, name: p.player.name, currentElo: p.player.currentElo, })); // Check minimum participants if (participants.length < 2) { return NextResponse.json( { error: "At least 2 participants are required to generate a schedule" }, { status: 400 } ); } // Generate teams based on configuration const teamDurability = tournament.teamDurability || "permanent"; const partnerRotation = (tournament.partnerRotation || "none") as 'none' | 'minimize_repeat' | 'maximize_even' | 'elo_based'; const allowByes = tournament.allowByes ?? true; // Determine number of teams from participants const tempResult = generateTeams(participants, partnerRotation, allowByes); const teamCount = tempResult.teams.length; if (teamCount < 2) { return NextResponse.json( { error: "At least 2 teams (4 players) are required to generate a schedule" }, { status: 400 } ); } // Calculate number of rounds needed const numRounds = expectedRounds(teamCount); if (teamDurability === "permanent") { // ============================================ // OPTION 1: FIXED TEAMS // Teams are formed once and stay the same throughout // ============================================ // Generate teams once for permanent team tournaments const result = generateTeams(participants, partnerRotation, allowByes); const teamPairings = result.teams.map((t) => ({ player1Id: t.player1Id, player2Id: t.player2Id, })); // Validate schedule input const validation = validateScheduleInput(teamPairings); if (!validation.valid) { return NextResponse.json( { error: validation.error }, { status: 400 } ); } // Generate schedule using fixed teams const schedule = generateRoundRobin(teamPairings); // Create rounds and matchups in a transaction const created = await prisma.$transaction( schedule.map((round) => prisma.tournamentRound.create({ data: { eventId: tournamentId, roundNumber: round.roundNumber, status: "pending", bracketMatchups: { create: round.matchups.map((matchup, idx) => ({ eventId: tournamentId, player1P1Id: matchup.player1P1Id, player1P2Id: matchup.player1P2Id, player2P1Id: matchup.player2P1Id, player2P2Id: matchup.player2P2Id, bracketPosition: idx + 1, status: "pending", })), }, }, include: { bracketMatchups: { include: { player1P1: true, player1P2: true, player2P1: true, player2P2: true, }, }, }, }) ) ); return NextResponse.json({ success: true, roundsCreated: created.length, matchupsCreated: created.reduce( (sum, r) => sum + r.bracketMatchups.length, 0 ), rounds: created, }); } else if (teamDurability === "variable") { // ============================================ // OPTION 2: PRE-PLANNED VARIABLE // Fresh teams each round, generated before tournament starts // Partners rotate based on selected strategy // ============================================ // Track partnerships across all rounds to minimize repeats const allPreviousTeams: TeamPairing[][] = []; // Create a function that generates teams with rotation const generateTeamWithRotation = (players: Player[]): TeamPairing[] => { // For pre-planned variable, we generate fresh teams each round // using the partner rotation strategy and tracking previous partnerships const result = generateTeamsWithRotation(players, allPreviousTeams, partnerRotation, allowByes); // Store the generated teams for future rounds allPreviousTeams.push(result.teams); return result.teams; }; // Generate the schedule with fresh teams each round const schedule = generateVariableRoundRobin( participants, teamCount, numRounds, generateTeamWithRotation ); // Create rounds and matchups in a transaction const created = await prisma.$transaction( schedule.map((round) => prisma.tournamentRound.create({ data: { eventId: tournamentId, roundNumber: round.roundNumber, status: "pending", bracketMatchups: { create: round.matchups.map((matchup, idx) => ({ eventId: tournamentId, player1P1Id: matchup.player1P1Id, player1P2Id: matchup.player1P2Id, player2P1Id: matchup.player2P1Id, player2P2Id: matchup.player2P2Id, bracketPosition: idx + 1, status: "pending", })), }, }, include: { bracketMatchups: { include: { player1P1: true, player1P2: true, player2P1: true, player2P2: true, }, }, }, }) ) ); return NextResponse.json({ success: true, roundsCreated: created.length, matchupsCreated: created.reduce( (sum, r) => sum + r.bracketMatchups.length, 0 ), rounds: created, }); } else { // ============================================ // OPTION 3: DYNAMIC/PROGRESSIVE // Teams formed based on results (bracket-style) // Cannot pre-generate full schedule // ============================================ return NextResponse.json({ success: true, message: "Dynamic tournaments require completing rounds before scheduling the next one", roundsCreated: 0, matchupsCreated: 0, rounds: [], requiresDynamicScheduling: true, }); } } catch (error: unknown) { console.error("Error generating schedule:", error); const message = error instanceof Error ? error.message : "Failed to generate schedule"; return NextResponse.json({ error: message }, { status: 500 }); } } /** * DELETE /api/tournaments/[id]/schedule * * Delete all rounds and matchups for a tournament. */ export async function DELETE(_request: Request, { params }: RouteParams) { try { const { id } = await params; const tournamentId = parseInt(id); if (isNaN(tournamentId)) { return NextResponse.json( { error: "Invalid tournament ID" }, { status: 400 } ); } const permission = await canManageTournament(tournamentId); if (!permission.allowed) { return NextResponse.json( { error: permission.reason || "Not authorized to manage this tournament" }, { status: 403 } ); } // Delete bracket matchups first (FK constraint) const deletedMatchups = await prisma.bracketMatchup.deleteMany({ where: { eventId: tournamentId }, }); // Delete rounds const deletedRounds = await prisma.tournamentRound.deleteMany({ where: { eventId: tournamentId }, }); return NextResponse.json({ success: true, deletedRounds: deletedRounds.count, deletedMatchups: deletedMatchups.count, }); } catch (error: unknown) { console.error("Error deleting schedule:", error); const message = error instanceof Error ? error.message : "Failed to delete schedule"; return NextResponse.json({ error: message }, { status: 500 }); } }