import { GroupId, GroupTable, Match, Team, ThirdPlaceRow } from "@/lib/types"; import { R32, LATER_ROUNDS, R32Match, BracketSlot, ThirdAssignment, slotLabel, } from "@/lib/bracket"; // Ein aufgelöster Teilnehmer einer K.o.-Begegnung. export interface ResolvedSide { teamId: string | null; // null = noch Platzhalter label: string; // Anzeige (Teamname oder "Sieger E" etc.) code: string; score: number | null; isWinner: boolean; } export interface ResolvedTie { matchNumber: number; stage: string; home: ResolvedSide; away: ResolvedSide; status: string; prob?: { home: number; draw: number | null; away: number } | null; } // Findet das tatsächliche Spiel (aus dem Feed) zu einer FIFA-Match-Nummer. function feedMatch(matches: Match[], num: number): Match | undefined { return matches.find((m) => m.matchNumber === num && m.group == null); } // Bestimmt den Sieger eines abgeschlossenen Spiels (Feed) als Team-ID. function winnerOf(m: Match | undefined): string | null { if (!m || m.status !== "FINISHED") return null; if (m.homeScore == null || m.awayScore == null) return null; if (m.homeScore > m.awayScore) return m.homeTeamId; if (m.awayScore > m.homeScore) return m.awayTeamId; return null; // Unentschieden -> Elfmeter; Feed liefert i.d.R. Sieger separat } function loserOf(m: Match | undefined): string | null { if (!m || m.status !== "FINISHED") return null; if (m.homeScore == null || m.awayScore == null) return null; if (m.homeScore > m.awayScore) return m.awayTeamId; if (m.awayScore > m.homeScore) return m.homeTeamId; return null; } // Löst einen R32-Slot (W/R/3) zu einer Team-ID auf, sofern bereits bekannt. function resolveR32Slot( slot: BracketSlot, winnerGroup: GroupId | undefined, tables: GroupTable[], assignment: ThirdAssignment | null, thirds: ThirdPlaceRow[], ): string | null { const table = (g: GroupId) => tables.find((t) => t.group === g); if (slot.type === "W") { const t = table(slot.group!); return t?.rows.find((r) => r.rank === 1)?.teamId ?? null; } if (slot.type === "R") { const t = table(slot.group!); return t?.rows.find((r) => r.rank === 2)?.teamId ?? null; } // 3. Platz: braucht Annex-C-Zuordnung + den Gruppensieger dieses Spiels if (slot.type === "3" && assignment && winnerGroup) { const thirdGroup = assignment[winnerGroup]; if (thirdGroup) { const row = thirds.find((r) => r.group === thirdGroup && r.qualifies); return row?.teamId ?? null; } } return null; } function sideFrom( teamId: string | null, fallbackLabel: string, teams: Team[], feed: Match | undefined, which: "home" | "away", ): ResolvedSide { const team = teamId ? teams.find((t) => t.id === teamId) : undefined; const score = feed ? which === "home" ? feed.homeScore : feed.awayScore : null; const w = winnerOf(feed); return { teamId, label: team?.name ?? fallbackLabel, code: team?.code ?? "", score, isWinner: teamId != null && w === teamId, }; } // Baut die komplette aufgelöste Bracket-Struktur. export function resolveBracket( matches: Match[], teams: Team[], tables: GroupTable[], thirds: ThirdPlaceRow[], assignment: ThirdAssignment | null, ): { r32: ResolvedTie[]; later: Record } { // Map: Match-Nummer -> Sieger-Team-ID (für Propagation in Folgerunden) const winners = new Map(); const losers = new Map(); const r32: ResolvedTie[] = R32.map((rm: R32Match) => { const feed = feedMatch(matches, rm.matchNumber); const homeGroup = rm.home.type === "W" ? rm.home.group : undefined; const awayGroup = rm.away.type === "W" ? rm.away.group : undefined; // Für 3.-Platz-Slots: Gruppensieger ist die andere Seite (immer W) const winnerGroup = (homeGroup ?? awayGroup) as GroupId | undefined; const homeId = resolveR32Slot(rm.home, winnerGroup, tables, assignment, thirds); const awayId = resolveR32Slot(rm.away, winnerGroup, tables, assignment, thirds); const home = sideFrom(homeId, slotLabel(rm.home, assignment, winnerGroup), teams, feed, "home"); const away = sideFrom(awayId, slotLabel(rm.away, assignment, winnerGroup), teams, feed, "away"); winners.set(rm.matchNumber, winnerOf(feed)); losers.set(rm.matchNumber, loserOf(feed)); return { matchNumber: rm.matchNumber, stage: "R32", home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob, }; }); const later: Record = {}; for (const km of LATER_ROUNDS) { const feed = feedMatch(matches, km.matchNumber); const src = km.losers ? losers : winners; const homeId = src.get(km.fromHome) ?? null; const awayId = src.get(km.fromAway) ?? null; const homeLabel = `${km.losers ? "Verlierer" : "Sieger"} ${km.fromHome}`; const awayLabel = `${km.losers ? "Verlierer" : "Sieger"} ${km.fromAway}`; const home = sideFrom(homeId, homeLabel, teams, feed, "home"); const away = sideFrom(awayId, awayLabel, teams, feed, "away"); winners.set(km.matchNumber, winnerOf(feed)); losers.set(km.matchNumber, loserOf(feed)); later[km.matchNumber] = { matchNumber: km.matchNumber, stage: km.stage, home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob, }; } return { r32, later }; }