import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import Papa from "papaparse"; import { canManageTournament } from "@/lib/permissions"; import { getSession } from "@/lib/auth-simple"; import { calculateTeamElo, calculateTeamEloChange } from "@/lib/elo-utils"; interface CSVRow { "Event #": string; Round: string; Table: string; "Seat 1": string; "Seat 3": string; "Odds Points": string; "Seat 2": string; "Seat 4": string; "Evens Points": string; Winner?: string; } const K_FACTOR = 32; // Standard K-factor for Elo calculations export async function POST(request: Request) { try { const formData = await request.formData(); const csvFile = formData.get("csvFile") as File; const eventId = formData.get("eventId") as string; if (!csvFile) { return NextResponse.json( { error: "No CSV file provided" }, { status: 400 } ); } if (!eventId) { return NextResponse.json( { error: "No tournament selected" }, { status: 400 } ); } // Check if user has permission to add matches to this tournament const permission = await canManageTournament(parseInt(eventId)); if (!permission.allowed) { return NextResponse.json( { error: permission.reason || 'Insufficient permissions to add matches' }, { status: 403 } ); } // Get the current user to assign as match creator const session = await getSession(); const userId = session?.user?.id || null; // Read and parse CSV const csvText = await csvFile.text(); const parseResult = Papa.parse(csvText, { header: true, skipEmptyLines: true, }); if (parseResult.errors.length > 0) { return NextResponse.json( { error: "CSV parsing errors", errors: parseResult.errors.map((e: { message: string }) => e.message) }, { status: 400 } ); } const rows = parseResult.data; let importedCount = 0; let errorCount = 0; const errors: string[] = []; // Process each row for (const [index, row] of rows.entries()) { try { // Extract player names const player1Name = row["Seat 1"]?.trim(); const player2Name = row["Seat 3"]?.trim(); const player3Name = row["Seat 2"]?.trim(); const player4Name = row["Seat 4"]?.trim(); if (!player1Name || !player2Name || !player3Name || !player4Name) { errors.push(`Row ${index + 2}: Missing player names`); errorCount++; continue; } // Find or create players const players = await Promise.all([ findOrCreatePlayer(player1Name), findOrCreatePlayer(player2Name), findOrCreatePlayer(player3Name), findOrCreatePlayer(player4Name), ]); console.log('Players found/created:'); players.forEach((p, i) => { console.log(` Player ${i + 1}: ${p.name} (ID: ${p.id}, Elo: ${p.currentElo})`); }); // Parse scores const team1Score = parseInt(row["Odds Points"]) || 0; const team2Score = parseInt(row["Evens Points"]) || 0; // Determine winner const team1Won = team1Score > team2Score; const team2Won = team2Score > team1Score; const isTie = team1Score === team2Score; // Calculate Elo ratings using standard formula const team1Rating = calculateTeamElo(players[0].currentElo, players[1].currentElo); const team2Rating = calculateTeamElo(players[2].currentElo, players[3].currentElo); // Actual scores (1 for win, 0.5 for tie, 0 for loss) const team1ScoreActual = isTie ? 0.5 : (team1Won ? 1 : 0); const team2ScoreActual = isTie ? 0.5 : (team2Won ? 1 : 0); // Elo change for each team const team1EloChange = calculateTeamEloChange(team1Rating, team2Rating, team1ScoreActual, K_FACTOR); const team2EloChange = calculateTeamEloChange(team2Rating, team1Rating, team2ScoreActual, K_FACTOR); // Individual Elo changes (split evenly between team members) const player1EloChange = team1EloChange / 2; const player2EloChange = team1EloChange / 2; const player3EloChange = team2EloChange / 2; const player4EloChange = team2EloChange / 2; // Create match await prisma.match.create({ data: { eventId: parseInt(eventId), playedAt: new Date(), player1P1Id: players[0].id, player1P2Id: players[1].id, player2P1Id: players[2].id, player2P2Id: players[3].id, team1Score, team2Score, status: "completed", createdById: userId, }, }); // Update individual player stats await updatePlayerStats(players[0].id, team1Won, player1EloChange); await updatePlayerStats(players[1].id, team1Won, player2EloChange); await updatePlayerStats(players[2].id, team2Won, player3EloChange); await updatePlayerStats(players[3].id, team2Won, player4EloChange); // Update partnership stats (for tracking how players perform together) await updatePartnershipStats(players[0].id, players[1].id, team1Won, player1EloChange); await updatePartnershipStats(players[2].id, players[3].id, team2Won, player3EloChange); // Add players as participants in the tournament (if not already added) for (const player of players) { try { await prisma.eventParticipant.create({ data: { eventId: parseInt(eventId), playerId: player.id, status: "participated", registrationDate: new Date(), }, }); } catch { // Participant already exists, which is fine console.log(`Participant ${player.id} already exists in tournament ${eventId}`); } } importedCount++; } catch (err: unknown) { const message = err instanceof Error ? err.message : "Unknown error"; errors.push(`Row ${index + 2}: ${message}`); errorCount++; } } return NextResponse.json({ importedCount, errorCount, errors: errors.length > 0 ? errors : undefined, }); } catch (error: unknown) { console.error("Upload error:", error); const message = error instanceof Error ? error.message : "Failed to upload CSV"; return NextResponse.json( { error: message }, { status: 500 } ); } } async function findOrCreatePlayer(name: string) { // Normalize the name for deduplication: trim whitespace and convert to lowercase const normalizedName = name.trim().toLowerCase(); // Try to find existing player with matching normalized name let player = await prisma.player.findFirst({ where: { normalizedName }, }); if (!player) { // Create a new player with the original name and normalized name player = await prisma.player.create({ data: { name: name.trim(), normalizedName, rating: 1000, currentElo: 1000, }, }); } return player; } async function updatePlayerStats(playerId: number, won: boolean, eloChange: number) { // First, get the current player data to calculate the new values const player = await prisma.player.findUnique({ where: { id: playerId }, }); if (!player) { throw new Error(`Player ${playerId} not found`); } // Update the player's statistics await prisma.player.update({ where: { id: playerId }, data: { currentElo: player.currentElo + Math.round(eloChange), gamesPlayed: player.gamesPlayed + 1, wins: won ? player.wins + 1 : player.wins, losses: won ? player.losses : player.losses + 1, }, }); } async function updatePartnershipStats(player1Id: number, player2Id: number, won: boolean, eloChange: number) { // Sort IDs to ensure consistent partnership lookup const [smallId, largeId] = player1Id < player2Id ? [player1Id, player2Id] : [player2Id, player1Id]; // Find existing partnership const existing = await prisma.partnershipStat.findFirst({ where: { player1Id: smallId, player2Id: largeId, }, }); if (existing) { // Update existing partnership await prisma.partnershipStat.update({ where: { id: existing.id }, data: { gamesPlayed: { increment: 1 }, wins: won ? { increment: 1 } : undefined, losses: won ? undefined : { increment: 1 }, totalEloChange: { increment: eloChange }, lastPlayed: new Date(), }, }); } else { // Create new partnership await prisma.partnershipStat.create({ data: { player1Id: smallId, player2Id: largeId, gamesPlayed: 1, wins: won ? 1 : 0, losses: won ? 0 : 1, totalEloChange: eloChange, lastPlayed: new Date(), }, }); } }