import { and, eq, notInArray } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import type { Simulator, SimulationProbabilities, SimulationResult } from "./types";

const PROB_KEYS = [
  "probFirst",
  "probSecond",
  "probThird",
  "probFourth",
  "probFifth",
  "probSixth",
  "probSeventh",
  "probEighth",
] as const;

function emptyProbabilities(): SimulationProbabilities {
  return {
    probFirst: 0,
    probSecond: 0,
    probThird: 0,
    probFourth: 0,
    probFifth: 0,
    probSixth: 0,
    probSeventh: 0,
    probEighth: 0,
  };
}

export function harvillePlacementProbabilities(weights: number[]): number[][] {
  const n = weights.length;
  const maxPlaces = Math.min(8, n);
  if (n === 0) return [];

  if (weights.every((w) => w <= 0)) {
    return weights.map(() =>
      Array.from({ length: maxPlaces }, () => Number((1 / n).toFixed(6)))
    );
  }

  const probs = weights.map(() => Array.from({ length: maxPlaces }, () => 0));
  let states = new Map<number, number>([[0, 1]]);

  for (let place = 0; place < maxPlaces; place++) {
    const next = new Map<number, number>();
    for (const [mask, stateProb] of states) {
      const remaining = weights
        .map((weight, index) => ({ weight: Math.max(0, weight), index }))
        .filter(({ index }) => (mask & (1 << index)) === 0);
      const total = remaining.reduce((sum, item) => sum + item.weight, 0);

      for (const item of remaining) {
        const pickProb = total > 0 ? item.weight / total : 1 / remaining.length;
        const probability = stateProb * pickProb;
        probs[item.index][place] += probability;
        const nextMask = mask | (1 << item.index);
        next.set(nextMask, (next.get(nextMask) ?? 0) + probability);
      }
    }
    states = next;
  }

  return probs;
}

export class BracktSimulator implements Simulator {
  private readonly db: ReturnType<typeof database>;

  constructor(providedDb?: ReturnType<typeof database>) {
    this.db = providedDb ?? database();
  }

  async simulate(sportsSeasonId: string): Promise<SimulationResult[]> {
    const db = this.db;
    const bracktSeason = await db.query.sportsSeasons.findFirst({
      where: eq(schema.sportsSeasons.id, sportsSeasonId),
      with: { sport: true },
    });

    if (!bracktSeason?.fantasySeasonId) {
      throw new Error("Brackt simulation requires a private sports season linked to a fantasy season");
    }

    const fantasySeasonId = bracktSeason.fantasySeasonId;
    const [teams, bracktParticipants, seasonSports, draftPicks] = await Promise.all([
      db.query.teams.findMany({
        where: eq(schema.teams.seasonId, fantasySeasonId),
        orderBy: (teamsTable, { asc }) => [asc(teamsTable.name)],
      }),
      db.query.seasonParticipants.findMany({
        where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
      }),
      db.query.seasonSports.findMany({
        where: eq(schema.seasonSports.seasonId, fantasySeasonId),
        with: { sportsSeason: { with: { sport: true } } },
      }),
      db.query.draftPicks.findMany({
        where: eq(schema.draftPicks.seasonId, fantasySeasonId),
        with: { participant: { with: { sportsSeason: { with: { sport: true } } } } },
      }),
    ]);

    if (teams.length < 6 || teams.length > 16) {
      throw new Error("Brackt supports leagues with 6 to 16 teams");
    }

    const nonBracktSeasonIds = seasonSports
      .filter((link) => link.sportsSeason.sport.slug !== "brackt")
      .map((link) => link.sportsSeasonId);

    const draftedIds = draftPicks.map((pick) => pick.participantId);
    const availableBySportsSeason = new Map<string, number>();

    if (nonBracktSeasonIds.length > 0) {
      for (const nonBracktSeasonId of nonBracktSeasonIds) {
        const whereClause = draftedIds.length > 0
          ? and(
              eq(schema.seasonParticipants.sportsSeasonId, nonBracktSeasonId),
              notInArray(schema.seasonParticipants.id, draftedIds)
            )
          : eq(schema.seasonParticipants.sportsSeasonId, nonBracktSeasonId);
        const available = await db.query.seasonParticipants.findMany({ where: whereClause });
        const average = available.length > 0
          ? available.reduce((sum, p) => sum + Number(p.expectedValue ?? 0), 0) / available.length
          : 0;
        availableBySportsSeason.set(nonBracktSeasonId, average);
      }
    }

    const picksByTeam = new Map<string, typeof draftPicks>();
    for (const pick of draftPicks) {
      const existing = picksByTeam.get(pick.teamId) ?? [];
      existing.push(pick);
      picksByTeam.set(pick.teamId, existing);
    }

    const weights = teams.map((team) => {
      const teamPicks = picksByTeam.get(team.id) ?? [];
      const draftedBySportsSeason = new Set<string>();
      let projection = 0;

      for (const pick of teamPicks) {
        if (pick.participant.sportsSeason.sport.slug === "brackt") continue;
        draftedBySportsSeason.add(pick.participant.sportsSeasonId);
        projection += Number(pick.participant.expectedValue ?? 0);
      }

      for (const nonBracktSeasonId of nonBracktSeasonIds) {
        if (!draftedBySportsSeason.has(nonBracktSeasonId)) {
          projection += availableBySportsSeason.get(nonBracktSeasonId) ?? 0;
        }
      }

      return Math.max(0, projection);
    });

    const placementProbabilities = harvillePlacementProbabilities(weights);
    const participantByTeamId = new Map(bracktParticipants.map((p) => [p.externalId, p]));

    return teams.map((team, teamIndex) => {
      const participant = participantByTeamId.get(team.id);
      if (!participant) {
        throw new Error(`Missing Brackt participant for team ${team.name}`);
      }
      const probabilities = emptyProbabilities();
      for (let i = 0; i < Math.min(PROB_KEYS.length, placementProbabilities[teamIndex].length); i++) {
        probabilities[PROB_KEYS[i]] = Number(placementProbabilities[teamIndex][i].toFixed(4));
      }
      return {
        participantId: participant.id,
        probabilities,
        source: "brackt_harville",
      };
    });
  }
}
