import { and, eq, inArray } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import {
  buildNCAA68SlotMap,
  matchIndexForSeedSlot,
  NCAA_68,
  STANDARD_BRACKET_SEEDING,
  type BracketRegion,
} from "~/lib/bracket-templates";
import { convertAmericanOddsToProbability } from "~/services/probability-engine";
import type { Simulator, SimulationResult } from "./types";

const NUM_SIMULATIONS = 50_000;
const FIELD_SIZE = 68;
const ODDS_SELECTION_BONUS = 0.1;

type PlayoffMatch = typeof schema.playoffMatches.$inferSelect;
type Db = ReturnType<typeof database>;

interface Team {
  participantId: string;
  name: string;
  rating: number;
  sourceOdds: number | null;
}

interface PlacementCounts {
  champion: number;
  finalist: number;
  finalFourLoser: number;
  eliteEightLoser: number;
}

interface NCAARegionAssignment {
  directSeeds: Map<number, Team>;
  playIns: Map<number, [Team, Team]>;
}

interface NCAABasketballSimulatorOptions {
  source: string;
  missingRatingName: string;
  selectionRatingDivisor: number;
  winProbability: (ratingA: number, ratingB: number) => number;
  getWinProbability?: (config: Record<string, unknown>) => (ratingA: number, ratingB: number) => number;
}

export interface FirstFourMapping {
  firstFourMatchNumber: number;
  r64MatchNumber: number;
  seedSlot: number;
  regionName: string;
}

export function buildFirstFourMappings(regions: BracketRegion[] = NCAA_68.regions ?? []): FirstFourMapping[] {
  const slotMap = buildNCAA68SlotMap(regions);
  return slotMap.playInOffsets.map((playIn, index) => ({
    firstFourMatchNumber: index + 1,
    r64MatchNumber: playIn.regionIndex * 8 + matchIndexForSeedSlot(playIn.seedSlot) + 1,
    seedSlot: playIn.seedSlot,
    regionName: regions[playIn.regionIndex]?.name ?? `Region ${playIn.regionIndex + 1}`,
  }));
}

function sortByMatchNumber<T extends { matchNumber: number }>(matches: T[]): T[] {
  return [...matches].toSorted((a, b) => a.matchNumber - b.matchNumber);
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

async function getSeasonSimulatorConfig(db: Db, sportsSeasonId: string): Promise<Record<string, unknown>> {
  const season = await db.query.sportsSeasons.findFirst({
    where: eq(schema.sportsSeasons.id, sportsSeasonId),
    with: {
      sport: true,
      simulatorConfig: true,
    },
  });
  const simulatorType = season?.simulatorConfig?.simulatorType ?? season?.sport?.simulatorType;
  if (!simulatorType) return {};

  const profile = await db.query.simulatorProfiles.findFirst({
    where: eq(schema.simulatorProfiles.simulatorType, simulatorType),
    columns: { defaultConfig: true },
  });

  return {
    ...(isRecord(profile?.defaultConfig) ? profile.defaultConfig : {}),
    ...(isRecord(season?.simulatorConfig?.config) ? season.simulatorConfig.config : {}),
  };
}

function bump(counts: Map<string, PlacementCounts>, participantId: string | null | undefined, key: keyof PlacementCounts): void {
  if (!participantId) return;
  const entry = counts.get(participantId);
  if (entry) entry[key]++;
}

function makeCounts(participantIds: string[]): Map<string, PlacementCounts> {
  return new Map(
    participantIds.map((participantId) => [
      participantId,
      { champion: 0, finalist: 0, finalFourLoser: 0, eliteEightLoser: 0 },
    ])
  );
}

function completedLoser(match: PlayoffMatch): string {
  if (match.loserId) return match.loserId;
  if (match.participant1Id === match.winnerId && match.participant2Id) return match.participant2Id;
  if (match.participant2Id === match.winnerId && match.participant1Id) return match.participant1Id;
  throw new Error(`Completed ${match.round} match ${match.matchNumber} is missing a loser.`);
}

function normalizeOdds(teams: Team[]): Map<string, number> {
  const raw = new Map<string, number>();
  for (const team of teams) {
    if (team.sourceOdds !== null) {
      raw.set(team.participantId, convertAmericanOddsToProbability(team.sourceOdds));
    }
  }

  const total = [...raw.values()].reduce((sum, value) => sum + value, 0);
  const normalized = new Map<string, number>();
  if (total <= 0) return normalized;

  for (const [participantId, probability] of raw) {
    normalized.set(participantId, probability / total);
  }
  return normalized;
}

function buildSelectionWeights(teams: Team[], selectionRatingDivisor: number): Map<string, number> {
  const normalizedOdds = normalizeOdds(teams);
  const hasOdds = normalizedOdds.size > 0;
  const maxRating = Math.max(...teams.map((team) => team.rating));
  const divisor = Math.max(selectionRatingDivisor, 0.0001);

  const ratingWeights = new Map<string, number>();
  for (const team of teams) {
    ratingWeights.set(team.participantId, Math.max(Math.exp((team.rating - maxRating) / divisor), 0.000001));
  }

  const ratingTotal = [...ratingWeights.values()].reduce((sum, value) => sum + value, 0);
  const weights = new Map<string, number>();
  for (const team of teams) {
    const ratingShare = (ratingWeights.get(team.participantId) ?? 0.000001) / ratingTotal;
    const weight = hasOdds
      ? (normalizedOdds.get(team.participantId) ?? 0) + ODDS_SELECTION_BONUS * ratingShare
      : ratingShare;
    weights.set(team.participantId, Math.max(weight, 0.000001));
  }

  return weights;
}

function sampleField(teams: Team[], fieldSize: number, selectionRatingDivisor: number): Team[] {
  const weights = buildSelectionWeights(teams, selectionRatingDivisor);
  const remaining = [...teams];
  const selected: Team[] = [];

  for (let i = 0; i < fieldSize; i++) {
    const totalWeight = remaining.reduce((sum, team) => sum + (weights.get(team.participantId) ?? 0), 0);
    let cursor = Math.random() * totalWeight;
    let index = 0;
    for (; index < remaining.length - 1; index++) {
      cursor -= weights.get(remaining[index].participantId) ?? 0;
      if (cursor <= 0) break;
    }
    selected.push(remaining[index]);
    remaining.splice(index, 1);
  }

  return selected;
}

export function assignFieldToNCAA68Regions(
  field: Team[],
  regions: BracketRegion[] = NCAA_68.regions ?? []
): NCAARegionAssignment[] {
  if (field.length !== FIELD_SIZE) {
    throw new Error(`Expected ${FIELD_SIZE} teams for NCAA preseason field, received ${field.length}.`);
  }

  const assignments = regions.map((): NCAARegionAssignment => ({
    directSeeds: new Map<number, Team>(),
    playIns: new Map<number, [Team, Team]>(),
  }));

  const sorted = field
    .map((team) => ({ team, tiebreaker: Math.random() }))
    .toSorted((a, b) => b.team.rating - a.team.rating || b.tiebreaker - a.tiebreaker)
    .map((entry) => entry.team);

  let cursor = 0;
  for (const seed of Array.from({ length: 16 }, (_, index) => index + 1)) {
    for (let regionIndex = 0; regionIndex < regions.length; regionIndex++) {
      const region = regions[regionIndex];
      const playIn = region.playIns.find((entry) => entry.seedSlot === seed);
      if (playIn) {
        assignments[regionIndex].playIns.set(seed, [sorted[cursor], sorted[cursor + 1]]);
        cursor += 2;
      } else {
        assignments[regionIndex].directSeeds.set(seed, sorted[cursor]);
        cursor++;
      }
    }
  }

  return assignments;
}

function simulateGame(
  team1: Team,
  team2: Team,
  winProbability: (ratingA: number, ratingB: number) => number
): { winner: Team; loser: Team } {
  const winner = Math.random() < winProbability(team1.rating, team2.rating) ? team1 : team2;
  return { winner, loser: winner === team1 ? team2 : team1 };
}

function buildPreseasonRoundOf64(
  assignments: NCAARegionAssignment[],
  winProbability: (ratingA: number, ratingB: number) => number
): Array<[Team, Team]> {
  const r64: Array<[Team, Team]> = [];

  for (const region of assignments) {
    for (const [seedA, seedB] of STANDARD_BRACKET_SEEDING) {
      const directA = region.directSeeds.get(seedA);
      const directB = region.directSeeds.get(seedB);
      const playInA = region.playIns.get(seedA);
      const playInB = region.playIns.get(seedB);
      const teamA = directA ?? (playInA ? simulateGame(playInA[0], playInA[1], winProbability).winner : null);
      const teamB = directB ?? (playInB ? simulateGame(playInB[0], playInB[1], winProbability).winner : null);

      if (!teamA || !teamB) {
        throw new Error(`NCAA preseason field is missing a Round of 64 team for ${seedA} vs ${seedB}.`);
      }
      r64.push([teamA, teamB]);
    }
  }

  return r64;
}

function simulateRoundOf64Bracket(
  r64Pairs: Array<[Team, Team]>,
  counts: Map<string, PlacementCounts>,
  winProbability: (ratingA: number, ratingB: number) => number
): void {
  const r64Winners = r64Pairs.map(([team1, team2]) => simulateGame(team1, team2, winProbability).winner);

  const r32Winners: Team[] = [];
  for (let i = 0; i < 16; i++) {
    r32Winners.push(simulateGame(r64Winners[i * 2], r64Winners[i * 2 + 1], winProbability).winner);
  }

  const s16Winners: Team[] = [];
  for (let i = 0; i < 8; i++) {
    s16Winners.push(simulateGame(r32Winners[i * 2], r32Winners[i * 2 + 1], winProbability).winner);
  }

  const e8Winners: Team[] = [];
  for (let i = 0; i < 4; i++) {
    const result = simulateGame(s16Winners[i * 2], s16Winners[i * 2 + 1], winProbability);
    e8Winners.push(result.winner);
    bump(counts, result.loser.participantId, "eliteEightLoser");
  }

  const ffWinners: Team[] = [];
  for (let i = 0; i < 2; i++) {
    const result = simulateGame(e8Winners[i * 2], e8Winners[i * 2 + 1], winProbability);
    ffWinners.push(result.winner);
    bump(counts, result.loser.participantId, "finalFourLoser");
  }

  const championship = simulateGame(ffWinners[0], ffWinners[1], winProbability);
  bump(counts, championship.winner.participantId, "champion");
  bump(counts, championship.loser.participantId, "finalist");
}

function countsToResults(participantIds: string[], counts: Map<string, PlacementCounts>, source: string): SimulationResult[] {
  const N = NUM_SIMULATIONS;
  return participantIds.map((participantId) => {
    const count = counts.get(participantId) ?? { champion: 0, finalist: 0, finalFourLoser: 0, eliteEightLoser: 0 };
    return {
      participantId,
      probabilities: {
        probFirst: count.champion / N,
        probSecond: count.finalist / N,
        probThird: count.finalFourLoser / (2 * N),
        probFourth: count.finalFourLoser / (2 * N),
        probFifth: count.eliteEightLoser / (4 * N),
        probSixth: count.eliteEightLoser / (4 * N),
        probSeventh: count.eliteEightLoser / (4 * N),
        probEighth: count.eliteEightLoser / (4 * N),
      },
      source,
    };
  });
}

async function loadTeams(
  db: Db,
  sportsSeasonId: string,
  participantIds?: string[],
  missingRatingName = "rating"
): Promise<Team[]> {
  const participantRows = participantIds
    ? await db
        .select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
        .from(schema.seasonParticipants)
        .where(inArray(schema.seasonParticipants.id, participantIds))
    : await db
        .select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
        .from(schema.seasonParticipants)
        .where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId));

  const simulatorInputs = await db
    .select({
      participantId: schema.seasonParticipantSimulatorInputs.participantId,
      rating: schema.seasonParticipantSimulatorInputs.rating,
      sourceOdds: schema.seasonParticipantSimulatorInputs.sourceOdds,
    })
    .from(schema.seasonParticipantSimulatorInputs)
    .where(eq(schema.seasonParticipantSimulatorInputs.sportsSeasonId, sportsSeasonId));

  const inputById = new Map(simulatorInputs.map((input) => [input.participantId, input]));
  const foundIds = new Set(participantRows.map((row) => row.id));
  const missingParticipants = participantIds?.filter((participantId) => !foundIds.has(participantId)) ?? [];
  if (missingParticipants.length > 0) {
    throw new Error(`Bracket includes ${missingParticipants.length} participant(s) that are not in this sports season.`);
  }

  const teams = participantRows.map((participant): Team => {
    const input = inputById.get(participant.id);
    const rating = input?.rating !== null && input?.rating !== undefined ? Number(input.rating) : null;
    if (rating === null || !Number.isFinite(rating)) {
      throw new Error(
        `${participant.name} is missing a prepared ${missingRatingName}. ` +
          `Enter direct ratings, futures odds, or configure an explicit rating fallback before running this simulator.`
      );
    }

    return {
      participantId: participant.id,
      name: participant.name,
      rating,
      sourceOdds: input?.sourceOdds ?? null,
    };
  });

  return participantIds
    ? participantIds.map((participantId) => {
        const team = teams.find((candidate) => candidate.participantId === participantId);
        if (!team) throw new Error(`Participant ${participantId} is missing from the prepared team map.`);
        return team;
      })
    : teams;
}

function groupMatchesByRound(matches: PlayoffMatch[]): {
  firstFourMatches: PlayoffMatch[];
  r64Matches: PlayoffMatch[];
  r32Matches: PlayoffMatch[];
  s16Matches: PlayoffMatch[];
  e8Matches: PlayoffMatch[];
  ffMatches: PlayoffMatch[];
  champMatch: PlayoffMatch;
} {
  const byRound = new Map<string, PlayoffMatch[]>();
  for (const match of matches) {
    if (!byRound.has(match.round)) byRound.set(match.round, []);
    byRound.get(match.round)?.push(match);
  }

  const firstFourMatches = sortByMatchNumber(byRound.get("First Four") ?? []);
  const r64Matches = sortByMatchNumber(byRound.get("Round of 64") ?? []);
  const r32Matches = sortByMatchNumber(byRound.get("Round of 32") ?? []);
  const s16Matches = sortByMatchNumber(byRound.get("Sweet Sixteen") ?? []);
  const e8Matches = sortByMatchNumber(byRound.get("Elite Eight") ?? []);
  const ffMatches = sortByMatchNumber(byRound.get("Final Four") ?? []);
  const champMatches = sortByMatchNumber(byRound.get("Championship") ?? []);

  if (
    r64Matches.length !== 32 ||
    r32Matches.length !== 16 ||
    s16Matches.length !== 8 ||
    e8Matches.length !== 4 ||
    ffMatches.length !== 2 ||
    champMatches.length !== 1
  ) {
    const found = [...byRound.keys()].join(", ");
    throw new Error(
      `Missing expected NCAA rounds. Found: [${found}]. ` +
        `Required: Round of 64, Round of 32, Sweet Sixteen, Elite Eight, Final Four, Championship.`
    );
  }

  return { firstFourMatches, r64Matches, r32Matches, s16Matches, e8Matches, ffMatches, champMatch: champMatches[0] };
}

function validateFirstFour(
  firstFourMatches: PlayoffMatch[],
  r64Matches: PlayoffMatch[],
  regions: BracketRegion[]
): Map<number, number> {
  const ffToR64 = new Map<number, number>();
  const r64NullSlots = new Set<number>(
    r64Matches.filter((match) => !match.participant2Id).map((match) => match.matchNumber)
  );

  if (firstFourMatches.length === 0) {
    for (const match of r64Matches) {
      if (!match.participant1Id || !match.participant2Id) {
        throw new Error(
          `Round of 64 match ${match.matchNumber} is missing participants. ` +
            `Assign all 64 teams to the bracket before running simulation.`
        );
      }
    }
    return ffToR64;
  }

  for (const mapping of buildFirstFourMappings(regions)) {
    ffToR64.set(mapping.firstFourMatchNumber, mapping.r64MatchNumber);
  }

  for (const match of firstFourMatches) {
    if (!match.participant1Id || !match.participant2Id) {
      throw new Error(`First Four match ${match.matchNumber} is missing participants. Assign both teams before running simulation.`);
    }
  }

  const r64SlotsCoveredByFF = new Set(ffToR64.values());
  for (const nullSlot of r64NullSlots) {
    if (!r64SlotsCoveredByFF.has(nullSlot)) {
      throw new Error(`Round of 64 match ${nullSlot} has no participant2 but is not covered by any First Four mapping.`);
    }
  }

  for (const [ffMatchNumber, r64MatchNumber] of ffToR64) {
    if (!r64NullSlots.has(r64MatchNumber)) {
      const ffMatch = firstFourMatches.find((match) => match.matchNumber === ffMatchNumber);
      if (!ffMatch?.isComplete) {
        throw new Error(
          `First Four maps to Round of 64 match ${r64MatchNumber}, but that slot is already filled. Check the bracket configuration.`
        );
      }
    }
  }

  for (const match of r64Matches) {
    if (!match.participant1Id) {
      throw new Error(`Round of 64 match ${match.matchNumber} is missing participant1. Check the bracket configuration.`);
    }
  }

  return ffToR64;
}

function getTeam(teamById: Map<string, Team>, participantId: string): Team {
  const team = teamById.get(participantId);
  if (!team) throw new Error(`Participant ${participantId} is missing a prepared rating row.`);
  return team;
}

function simulateMatchFromIds(
  participant1Id: string,
  participant2Id: string,
  match: PlayoffMatch | undefined,
  teamById: Map<string, Team>,
  winProbability: (ratingA: number, ratingB: number) => number
): { winner: Team; loser: Team } {
  if (match?.isComplete && match.winnerId) {
    return { winner: getTeam(teamById, match.winnerId), loser: getTeam(teamById, completedLoser(match)) };
  }
  return simulateGame(getTeam(teamById, participant1Id), getTeam(teamById, participant2Id), winProbability);
}

export class NCAABasketballSimulator implements Simulator {
  constructor(private readonly options: NCAABasketballSimulatorOptions) {}

  async simulate(sportsSeasonId: string): Promise<SimulationResult[]> {
    const db = database();
    const config = await getSeasonSimulatorConfig(db, sportsSeasonId);
    const winProbability = this.options.getWinProbability?.(config) ?? this.options.winProbability;
    const bracketEvent = await db.query.scoringEvents.findFirst({
      where: and(
        eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
        eq(schema.scoringEvents.eventType, "playoff_game")
      ),
    });

    if (!bracketEvent) {
      return this.simulatePreseason(db, sportsSeasonId, winProbability);
    }

    const allMatches = await db.query.playoffMatches.findMany({
      where: eq(schema.playoffMatches.scoringEventId, bracketEvent.id),
      orderBy: (match, { asc }) => [asc(match.matchNumber)],
    });

    if (allMatches.length === 0) {
      return this.simulatePreseason(db, sportsSeasonId, winProbability);
    }

    return this.simulateExistingBracket(
      db,
      sportsSeasonId,
      bracketEvent.bracketRegionConfig as BracketRegion[] | null,
      allMatches,
      winProbability
    );
  }

  private async simulatePreseason(
    db: Db,
    sportsSeasonId: string,
    winProbability: (ratingA: number, ratingB: number) => number
  ): Promise<SimulationResult[]> {
    const teams = await loadTeams(db, sportsSeasonId, undefined, this.options.missingRatingName);
    if (teams.length < FIELD_SIZE) {
      throw new Error(`NCAA preseason mode requires at least ${FIELD_SIZE} draftable participants, found ${teams.length}.`);
    }

    const participantIds = teams.map((team) => team.participantId);
    const counts = makeCounts(participantIds);

    for (let sim = 0; sim < NUM_SIMULATIONS; sim++) {
      const field = sampleField(teams, FIELD_SIZE, this.options.selectionRatingDivisor);
      const assignments = assignFieldToNCAA68Regions(field);
      const r64Pairs = buildPreseasonRoundOf64(assignments, winProbability);
      simulateRoundOf64Bracket(r64Pairs, counts, winProbability);
    }

    return countsToResults(participantIds, counts, `${this.options.source}_preseason`);
  }

  private async simulateExistingBracket(
    db: Db,
    sportsSeasonId: string,
    bracketRegions: BracketRegion[] | null,
    allMatches: PlayoffMatch[],
    winProbability: (ratingA: number, ratingB: number) => number
  ): Promise<SimulationResult[]> {
    const rounds = groupMatchesByRound(allMatches);
    const regions = bracketRegions ?? NCAA_68.regions ?? [];
    const ffToR64 = validateFirstFour(rounds.firstFourMatches, rounds.r64Matches, regions);

    const participantIdSet = new Set<string>();
    for (const match of rounds.r64Matches) {
      if (match.participant1Id) participantIdSet.add(match.participant1Id);
      if (match.participant2Id) participantIdSet.add(match.participant2Id);
    }
    for (const match of rounds.firstFourMatches) {
      if (match.participant1Id) participantIdSet.add(match.participant1Id);
      if (match.participant2Id) participantIdSet.add(match.participant2Id);
    }
    const participantIds = [...participantIdSet];
    const teams = await loadTeams(db, sportsSeasonId, participantIds, this.options.missingRatingName);
    const teamById = new Map(teams.map((team) => [team.participantId, team]));
    const counts = makeCounts(participantIds);

    const firstFourByNum = new Map(rounds.firstFourMatches.map((match) => [match.matchNumber, match]));
    const r64ByNum = new Map(rounds.r64Matches.map((match) => [match.matchNumber, match]));
    const r32ByNum = new Map(rounds.r32Matches.map((match) => [match.matchNumber, match]));
    const s16ByNum = new Map(rounds.s16Matches.map((match) => [match.matchNumber, match]));
    const e8ByNum = new Map(rounds.e8Matches.map((match) => [match.matchNumber, match]));
    const ffByNum = new Map(rounds.ffMatches.map((match) => [match.matchNumber, match]));

    for (let sim = 0; sim < NUM_SIMULATIONS; sim++) {
      const ffSimWinners = new Map<number, string>();
      for (const [ffMatchNumber, r64MatchNumber] of ffToR64) {
        const match = firstFourByNum.get(ffMatchNumber);
        if (!match?.participant1Id || !match.participant2Id) continue;
        const winner = match.isComplete && match.winnerId
          ? match.winnerId
          : simulateMatchFromIds(match.participant1Id, match.participant2Id, match, teamById, winProbability).winner.participantId;
        ffSimWinners.set(r64MatchNumber, winner);
      }

      const r64Winners: string[] = [];
      for (let i = 1; i <= 32; i++) {
        const match = r64ByNum.get(i);
        if (!match?.participant1Id) continue;
        const participant2Id = match.participant2Id ?? ffSimWinners.get(i);
        if (!participant2Id) throw new Error(`Round of 64 match ${i} is missing its second participant.`);
        const result = simulateMatchFromIds(match.participant1Id, participant2Id, match, teamById, winProbability);
        r64Winners.push(result.winner.participantId);
      }

      const r32Winners: string[] = [];
      for (let i = 1; i <= 16; i++) {
        const result = simulateMatchFromIds(
          r64Winners[(i - 1) * 2],
          r64Winners[(i - 1) * 2 + 1],
          r32ByNum.get(i),
          teamById,
          winProbability
        );
        r32Winners.push(result.winner.participantId);
      }

      const s16Winners: string[] = [];
      for (let i = 1; i <= 8; i++) {
        const result = simulateMatchFromIds(
          r32Winners[(i - 1) * 2],
          r32Winners[(i - 1) * 2 + 1],
          s16ByNum.get(i),
          teamById,
          winProbability
        );
        s16Winners.push(result.winner.participantId);
      }

      const e8Winners: string[] = [];
      for (let i = 1; i <= 4; i++) {
        const result = simulateMatchFromIds(
          s16Winners[(i - 1) * 2],
          s16Winners[(i - 1) * 2 + 1],
          e8ByNum.get(i),
          teamById,
          winProbability
        );
        e8Winners.push(result.winner.participantId);
        bump(counts, result.loser.participantId, "eliteEightLoser");
      }

      const ffWinners: string[] = [];
      for (let i = 1; i <= 2; i++) {
        const result = simulateMatchFromIds(
          e8Winners[(i - 1) * 2],
          e8Winners[(i - 1) * 2 + 1],
          ffByNum.get(i),
          teamById,
          winProbability
        );
        ffWinners.push(result.winner.participantId);
        bump(counts, result.loser.participantId, "finalFourLoser");
      }

      const final = simulateMatchFromIds(
        ffWinners[0],
        ffWinners[1],
        rounds.champMatch,
        teamById,
        winProbability
      );
      bump(counts, final.winner.participantId, "champion");
      bump(counts, final.loser.participantId, "finalist");
    }

    return countsToResults(participantIds, counts, this.options.source);
  }
}
