import { getAuth } from "@clerk/react-router/server"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and } from "drizzle-orm"; import { calculateDraftEligibility } from "~/lib/draft-eligibility"; import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/draft-pick"; import { getParticipantsForSeasonWithSports } from "~/models/participant"; import { getSeasonSportsSimple } from "~/models/season-sport"; export async function action(args: any) { const { request } = args; const auth = await getAuth(args); const userId = (auth as any).userId as string | null; if (!userId) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } const formData = await request.formData(); const seasonId = formData.get("seasonId") as string; const participantId = formData.get("participantId") as string; if (!seasonId || !participantId) { return Response.json({ error: "Missing required fields" }, { status: 400 }); } const db = database(); // Get season details const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), with: { league: true, }, }); if (!season) { return Response.json({ error: "Season not found" }, { status: 404 }); } // Get current draft slot (who should be picking now) const currentPickNumber = season.currentPickNumber || 1; const draftSlots = await db.query.draftSlots.findMany({ where: eq(schema.draftSlots.seasonId, seasonId), orderBy: schema.draftSlots.draftOrder, with: { team: true, }, }); const totalTeams = draftSlots.length; const currentRound = Math.ceil(currentPickNumber / totalTeams); const isSnakeDraft = true; // Assuming snake draft const isEvenRound = currentRound % 2 === 0; // Calculate which team should pick let pickInRound = ((currentPickNumber - 1) % totalTeams) + 1; if (isSnakeDraft && isEvenRound) { pickInRound = totalTeams - pickInRound + 1; } const currentDraftSlot = draftSlots.find((slot) => slot.draftOrder === pickInRound); if (!currentDraftSlot) { return Response.json({ error: "Invalid draft state" }, { status: 500 }); } // Check permissions: must be team owner or commissioner const isTeamOwner = currentDraftSlot.team.ownerId === userId; const isCommissioner = season.league.createdBy === userId; if (!isTeamOwner && !isCommissioner) { return Response.json({ error: "Not your turn to pick" }, { status: 403 }); } // Check if participant is already drafted const existingPick = await db.query.draftPicks.findFirst({ where: and( eq(schema.draftPicks.seasonId, seasonId), eq(schema.draftPicks.participantId, participantId) ), }); if (existingPick) { return Response.json({ error: "Participant already drafted" }, { status: 400 }); } // Get participant details const participant = await db.query.participants.findFirst({ where: eq(schema.participants.id, participantId), with: { sportsSeason: { with: { sport: true, }, }, }, }); if (!participant) { return Response.json({ error: "Participant not found" }, { status: 404 }); } // ELIGIBILITY VALIDATION: Check if team can draft from this sport const allPicks = await getDraftPicksWithSports(seasonId); const teamPicks = await getTeamDraftPicksWithSports(currentDraftSlot.teamId, seasonId); const allParticipants = await getParticipantsForSeasonWithSports(seasonId); const seasonSports = await getSeasonSportsSimple(seasonId); // Get all teams for the season const allTeams = draftSlots.map((slot) => ({ id: slot.teamId })); const eligibility = calculateDraftEligibility( currentDraftSlot.teamId, teamPicks, allPicks, allParticipants, seasonSports, season.draftRounds, allTeams ); const sportId = participant.sportsSeason.sport.id; if (!eligibility.eligibleSportIds.has(sportId)) { const reason = eligibility.ineligibleReasons[sportId] || "Cannot draft from this sport"; return Response.json({ error: reason }, { status: 400 }); } // Create the draft pick const [draftPick] = await db .insert(schema.draftPicks) .values({ seasonId, teamId: currentDraftSlot.teamId, participantId, pickNumber: currentPickNumber, round: currentRound, pickInRound, pickedByUserId: userId, pickedByType: isTeamOwner ? "owner" : "commissioner", }) .returning(); // Remove from ALL team queues in this season (participant is now drafted) await db .delete(schema.draftQueue) .where( and( eq(schema.draftQueue.seasonId, seasonId), eq(schema.draftQueue.participantId, participantId) ) ); // Notify all clients that this participant was removed from queues try { const io = (global as any).__socketIO; io.to(`draft-${seasonId}`).emit("participant-removed-from-queues", { participantId, }); } catch (error) { console.error("Socket.IO participant-removed-from-queues error:", error); } // Calculate next pick info (before updating season) const nextPickNumber = currentPickNumber + 1; const totalPicks = totalTeams * season.draftRounds; const isDraftComplete = nextPickNumber > totalPicks; // Add increment to the team that just picked const currentTimer = await db.query.draftTimers.findFirst({ where: and( eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, currentDraftSlot.teamId) ), }); if (currentTimer) { const incrementTime = season.draftIncrementTime || 30; const newTimeRemaining = currentTimer.timeRemaining + incrementTime; await db .update(schema.draftTimers) .set({ timeRemaining: newTimeRemaining, updatedAt: new Date(), }) .where(eq(schema.draftTimers.id, currentTimer.id)); // Emit timer update to all clients try { const io = (global as any).__socketIO; io.to(`draft-${seasonId}`).emit("timer-update", { seasonId, teamId: currentDraftSlot.teamId, timeRemaining: newTimeRemaining, currentPickNumber, }); } catch (error) { console.error("Socket.IO timer-update error:", error); } } // Initialize timer for the next team BEFORE updating pick number (prevents race condition) if (!isDraftComplete) { // Calculate which team is next using snake draft logic const nextRound = Math.ceil(nextPickNumber / totalTeams); const isNextRoundEven = nextRound % 2 === 0; let nextPickInRound = ((nextPickNumber - 1) % totalTeams) + 1; // Apply snake draft reversal for even rounds if (isSnakeDraft && isNextRoundEven) { nextPickInRound = totalTeams - nextPickInRound + 1; } const nextDraftSlot = draftSlots.find((slot) => slot.draftOrder === nextPickInRound); if (nextDraftSlot) { const nextTeamId = nextDraftSlot.teamId; const initialTime = season.draftInitialTime || 120; console.log( `[MakePick] Initializing timer for next team ${nextTeamId} with ${initialTime}s (pick ${nextPickNumber})` ); // Check if timer already exists for next team const nextTimer = await db.query.draftTimers.findFirst({ where: and( eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, nextTeamId) ), }); if (nextTimer) { // Update existing timer await db .update(schema.draftTimers) .set({ timeRemaining: initialTime, updatedAt: new Date(), }) .where(eq(schema.draftTimers.id, nextTimer.id)); } else { // Create new timer if it doesn't exist await db.insert(schema.draftTimers).values({ seasonId, teamId: nextTeamId, timeRemaining: initialTime, }); } // Emit timer update for next team try { const io = (global as any).__socketIO; io.to(`draft-${seasonId}`).emit("timer-update", { seasonId, teamId: nextTeamId, timeRemaining: initialTime, currentPickNumber: nextPickNumber, }); } catch (error) { console.error("Socket.IO next timer-update error:", error); } } } // Update season's current pick number (AFTER initializing next timer to prevent race condition) await db .update(schema.seasons) .set({ currentPickNumber: isDraftComplete ? currentPickNumber : nextPickNumber, status: isDraftComplete ? "active" : season.status, }) .where(eq(schema.seasons.id, seasonId)); // Emit socket event try { const io = (global as any).__socketIO; io.to(`draft-${seasonId}`).emit("pick-made", { pick: { ...draftPick, team: currentDraftSlot.team, participant: { ...participant, sport: participant.sportsSeason.sport, }, sport: participant.sportsSeason.sport, }, nextPickNumber: isDraftComplete ? currentPickNumber : nextPickNumber, isDraftComplete, }); if (isDraftComplete) { io.to(`draft-${seasonId}`).emit("draft-completed"); } } catch (error) { console.error("Socket.IO error:", error); } // Check if next team has autodraft enabled and trigger immediately if (!isDraftComplete) { const { checkAndTriggerNextAutodraft } = await import("~/models/draft-utils"); await checkAndTriggerNextAutodraft({ seasonId, nextPickNumber, totalTeams, draftSlots, db, }); } return Response.json({ success: true, pick: draftPick, nextPickNumber: isDraftComplete ? currentPickNumber : nextPickNumber, isDraftComplete, }); }