import { eq, inArray, count, and, sql, asc, desc } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; export type Participant = typeof schema.seasonParticipants.$inferSelect; export type NewParticipant = typeof schema.seasonParticipants.$inferInsert; export async function createParticipant(data: NewParticipant): Promise { const db = database(); // Auto-link to canonical participant (by sportId + name) if caller didn't // supply one. Needed so syncTournamentResults can match this roster entry // to canonical tournament_results; without the link the participant is // invisible to the canonical layer. let canonicalId = data.participantId; if (!canonicalId) { const season = await db.query.sportsSeasons.findFirst({ where: eq(schema.sportsSeasons.id, data.sportsSeasonId), columns: { sportId: true }, }); if (season) { const existing = await db.query.participants.findFirst({ where: and( eq(schema.participants.sportId, season.sportId), eq(schema.participants.name, data.name), ), }); if (existing) { canonicalId = existing.id; } else { const [created] = await db .insert(schema.participants) .values({ sportId: season.sportId, name: data.name }) .returning(); canonicalId = created.id; } } } const [participant] = await db .insert(schema.seasonParticipants) .values({ ...data, participantId: canonicalId }) .returning(); return participant; } export async function createManyParticipants(data: NewParticipant[]): Promise { const db = database(); if (data.length === 0) return []; // Build canonical links per row, just like createParticipant does. const seasonIds = Array.from(new Set(data.map((d) => d.sportsSeasonId))); const seasons = await db.query.sportsSeasons.findMany({ where: inArray(schema.sportsSeasons.id, seasonIds), columns: { id: true, sportId: true }, }); const sportBySeason = new Map(seasons.map((s) => [s.id, s.sportId])); const enriched: NewParticipant[] = []; for (const row of data) { if (row.participantId) { enriched.push(row); continue; } const sportId = sportBySeason.get(row.sportsSeasonId); if (!sportId) { enriched.push(row); continue; } const existing = await db.query.participants.findFirst({ where: and( eq(schema.participants.sportId, sportId), eq(schema.participants.name, row.name), ), }); let canonicalId = existing?.id; if (!canonicalId) { const [created] = await db .insert(schema.participants) .values({ sportId, name: row.name }) .returning(); canonicalId = created.id; } enriched.push({ ...row, participantId: canonicalId }); } return await db .insert(schema.seasonParticipants) .values(enriched) .returning(); } export async function findParticipantById(id: string): Promise { const db = database(); return await db.query.seasonParticipants.findFirst({ where: eq(schema.seasonParticipants.id, id), with: { sportsSeason: { with: { sport: true, }, }, }, }); } export async function findParticipantByName( sportsSeasonId: string, name: string ): Promise { const db = database(); const results = await db .select() .from(schema.seasonParticipants) .where( and( eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId), sql`lower(${schema.seasonParticipants.name}) = lower(${name})` ) ) .limit(1); return results[0]; } export async function findParticipantsBySportsSeasonId(sportsSeasonId: string): Promise { const db = database(); return await db.query.seasonParticipants.findMany({ where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId), orderBy: (participants, { asc: orderAsc }) => [orderAsc(participants.name)], }); } export async function findParticipantsByExternalId( externalId: string ): Promise { const db = database(); return await db.query.seasonParticipants.findMany({ where: eq(schema.seasonParticipants.externalId, externalId), with: { sportsSeason: { with: { sport: true, }, }, }, }); } export async function updateParticipant( id: string, data: Partial ): Promise { const db = database(); const [participant] = await db .update(schema.seasonParticipants) .set({ ...data, updatedAt: new Date() }) .where(eq(schema.seasonParticipants.id, id)) .returning(); return participant; } export async function countAllParticipants(): Promise { const db = database(); const result = await db.select({ value: count() }).from(schema.seasonParticipants); return result[0].value; } export async function deleteParticipant(id: string): Promise { const db = database(); await db.delete(schema.seasonParticipants).where(eq(schema.seasonParticipants.id, id)); } export interface CopyParticipantsResult { copied: number; skipped: number; evStubsCopied: number; } /** * Copy participants from one sports season to another, skipping duplicates by * name. Also copies EV stubs (sourceOdds, sourceElo, worldRanking) so that * running a simulation immediately after copying produces differentiated * results. * * Canonical skill data (golf SG:Total, tennis surface Elos) is automatically * available through the canonical participant link — no copying needed. */ export async function copyParticipantsFromSeason( sourceSportsSeasonId: string, targetSportsSeasonId: string, ): Promise { const db = database(); const targetParticipants = await findParticipantsBySportsSeasonId(targetSportsSeasonId); const targetNames = new Set(targetParticipants.map((p) => p.name.toLowerCase())); const sourceParticipants = await findParticipantsBySportsSeasonId(sourceSportsSeasonId); const toCopy = sourceParticipants.filter((p) => !targetNames.has(p.name.toLowerCase())); if (toCopy.length === 0) { return { copied: 0, skipped: sourceParticipants.length, evStubsCopied: 0 }; } const sourceEvRows = await db.query.seasonParticipantExpectedValues.findMany({ where: eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sourceSportsSeasonId), }); const sourceEvByKey = new Map(sourceEvRows.filter((r) => r.sourceOdds !== null || r.sourceElo !== null).map((r) => { const sp = sourceParticipants.find((p) => p.id === r.participantId); return sp ? [sp.externalId ?? sp.name, r] as const : null; }).filter((x): x is NonNullable => x !== null)); return await db.transaction(async (tx) => { const newParticipants = await tx .insert(schema.seasonParticipants) .values( toCopy.map((p) => ({ sportsSeasonId: targetSportsSeasonId, name: p.name, shortName: p.shortName, externalId: p.externalId, participantId: p.participantId, })), ) .returning(); let evStubsCopied = 0; if (sourceEvByKey.size > 0 && newParticipants.length > 0) { const now = new Date(); const evRows = newParticipants .map((newP) => { const key = newP.externalId ?? newP.name; const sourceEv = sourceEvByKey.get(key); if (!sourceEv) return null; return { participantId: newP.id, sportsSeasonId: targetSportsSeasonId, probFirst: "0", probSecond: "0", probThird: "0", probFourth: "0", probFifth: "0", probSixth: "0", probSeventh: "0", probEighth: "0", expectedValue: "0", source: sourceEv.source, sourceOdds: sourceEv.sourceOdds, sourceElo: sourceEv.sourceElo, worldRanking: sourceEv.worldRanking, calculatedAt: now, updatedAt: now, }; }) .filter((r): r is NonNullable => r !== null); if (evRows.length > 0) { await tx.insert(schema.seasonParticipantExpectedValues).values(evRows); evStubsCopied = evRows.length; } } return { copied: newParticipants.length, skipped: sourceParticipants.length - toCopy.length, evStubsCopied, }; }); } /** * Get all participants for a season with sport information * Used for draft eligibility calculations */ export async function getParticipantsForSeasonWithSports(seasonId: string, providedDb?: ReturnType) { const db = providedDb || database(); // First get all sports seasons for this season const seasonSports = await db.query.seasonSports.findMany({ where: eq(schema.seasonSports.seasonId, seasonId), }); if (seasonSports.length === 0) { return []; } const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId); // Get all participants for these sports seasons with sport info const participants = await db .select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name, sport: { id: schema.sports.id, name: schema.sports.name, }, }) .from(schema.seasonParticipants) .innerJoin( schema.sportsSeasons, eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id) ) .innerJoin( schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id) ) .where( sportsSeasonIds.length === 1 ? eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds[0]) : inArray(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds) ); return participants; } export async function getDraftParticipants(seasonId: string) { const db = database(); const seasonSports = await db.query.seasonSports.findMany({ where: eq(schema.seasonSports.seasonId, seasonId), }); const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId); if (sportsSeasonIds.length === 0) return []; return await db .select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name, vorpValue: schema.seasonParticipants.vorpValue, sport: schema.sports, }) .from(schema.seasonParticipants) .innerJoin(schema.sportsSeasons, eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id)) .innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id)) .where( sportsSeasonIds.length === 1 ? eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds[0]) : inArray(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds) ) .orderBy(desc(schema.seasonParticipants.vorpValue), asc(schema.seasonParticipants.name)); }