import { superTokensAdminFunctionMiddleware } from "@/utils/supertokens";
import { createServerFn } from "@tanstack/react-start";
import { pbAdmin } from "@/lib/pocketbase/client";
import { logger } from "@/lib/logger";
import { z } from "zod";
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
import brackets from "@/features/bracket/utils";
import { Match, MatchInput } from "@/features/matches/types";
import { emitServerEvent } from "@/lib/events/emitter";
import { superTokensFunctionMiddleware } from "@/utils/supertokens";
import { PlayerInfo } from "../players/types";
import { serverFnLoggingMiddleware } from "@/utils/activities";

const orderedTeamsSchema = z.object({
  tournamentId: z.string(),
  orderedTeamIds: z.array(z.string()),
});

export const generateTournamentBracket = createServerFn()
  .validator(orderedTeamsSchema)
  .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
  .handler(async ({ data: { tournamentId, orderedTeamIds } }) =>
    toServerResult(async () => {
      logger.info("Generating tournament bracket", {
        tournamentId,
        teamCount: orderedTeamIds.length,
      });

      const tournament = await pbAdmin.getTournament(tournamentId);
      if (!tournament) {
        throw new Error("Tournament not found");
      }

      if (tournament.matches && tournament.matches.length > 0) {
        throw new Error("Tournament already has matches generated");
      }

      const teamCount = orderedTeamIds.length;
      if (!Object.keys(brackets).includes(teamCount.toString())) {
        throw new Error(`Bracket not available for ${teamCount} teams`);
      }

      const bracketTemplate = brackets[
        teamCount as keyof typeof brackets
      ] as any;

      const seedToTeamId = new Map<number, string>();
      orderedTeamIds.forEach((teamId, index) => {
        seedToTeamId.set(index + 1, teamId);
      });

      const matchInputs: MatchInput[] = [];

      bracketTemplate.winners.forEach((round: any[]) => {
        round.forEach((match: any) => {
          const matchInput: MatchInput = {
            lid: match.lid,
            round: match.round,
            order: match.order || 0,
            reset: match.reset || false,
            bye: match.bye || false,
            home_cups: 0,
            away_cups: 0,
            ot_count: 0,
            home_from_lid: match.home_from_lid === null ? -1 : match.home_from_lid,
            away_from_lid: match.away_from_lid === null ? -1 : match.away_from_lid,
            home_from_loser: match.home_from_loser || false,
            away_from_loser: match.away_from_loser || false,
            is_losers_bracket: false,
            status: "tbd",
            tournament: tournamentId,
          };

          if (match.home_seed) {
            const teamId = seedToTeamId.get(match.home_seed);
            if (teamId) {
              matchInput.home = teamId;
              matchInput.home_seed = match.home_seed;
            }
          }

          if (match.away_seed) {
            const teamId = seedToTeamId.get(match.away_seed);
            if (teamId) {
              matchInput.away = teamId;
              matchInput.away_seed = match.away_seed;
            }
          }

          if (matchInput.home && matchInput.away) {
            matchInput.status = "ready";
          }

          matchInputs.push(matchInput);
        });
      });

      bracketTemplate.losers.forEach((round: any[]) => {
        round.forEach((match: any) => {
          const matchInput: MatchInput = {
            lid: match.lid,
            round: match.round,
            order: match.order || 0,
            reset: match.reset || false,
            bye: match.bye || false,
            home_cups: 0,
            away_cups: 0,
            ot_count: 0,
            home_from_lid: match.home_from_lid === null ? -1 : match.home_from_lid,
            away_from_lid: match.away_from_lid === null ? -1 : match.away_from_lid,
            home_from_loser: match.home_from_loser || false,
            away_from_loser: match.away_from_loser || false,
            is_losers_bracket: true,
            status: "tbd",
            tournament: tournamentId,
          };

          matchInputs.push(matchInput);
        });
      });

      const createdMatches = await pbAdmin.createMatches(matchInputs);

      const matchIds = createdMatches.map((match) => match.id);
      await pbAdmin.updateTournamentMatches(tournamentId, matchIds);

      logger.info("Tournament bracket generated", {
        tournamentId,
        matchCount: createdMatches.length,
      });

      emitServerEvent({ type: "tournament", tournamentId });

      return {
        tournament,
        matchCount: createdMatches.length,
        matches: createdMatches,
      };
    })
  );

export const startMatch = createServerFn()
  .validator(z.string())
  .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
  .handler(async ({ data }) =>
    toServerResult(async () => {
      logger.info("Starting match", data);

      let match = await pbAdmin.getMatch(data);
      if (!match) {
        throw new Error("Match not found");
      }

      match = await pbAdmin.updateMatch(data, {
        start_time: new Date().toISOString(),
        status: "started",
      });

      emitServerEvent({
        type: "match",
        matchId: match.id,
        tournamentId: match.tournament.id
      });

      return match;
    })
  );

export const populateKnockoutBracket = createServerFn()
  .validator(z.object({
    tournamentId: z.string(),
  }))
  .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
  .handler(async ({ data: { tournamentId } }) =>
    toServerResult(async () => {
      const tournament = await pbAdmin.getTournament(tournamentId);
      if (!tournament) {
        throw new Error("Tournament not found");
      }

      if (!tournament.group_config) {
        throw new Error("Tournament must have group_config");
      }

      const result = await populateKnockoutBracketInternal(tournamentId, tournament.group_config);
      emitServerEvent({ type: "tournament", tournamentId });
      return result;
    })
  );

async function populateKnockoutBracketInternal(tournamentId: string, groupConfig: { num_groups: number; advance_per_group: number }) {
  logger.info('Populating knockout bracket', { tournamentId });

  const groups = await pbAdmin.getGroupsByTournament(tournamentId);
  if (!groups || groups.length === 0) {
    throw new Error("No groups found for tournament");
  }

  const qualifiedTeams: { teamId: string; groupOrder: number; rank: number }[] = [];

  for (const group of groups) {
    logger.info('Processing group', {
      groupId: group.id,
      groupOrder: group.order,
      teamsCount: group.teams?.length,
      teams: group.teams
    });

    const groupMatches = await pbAdmin.getMatchesByGroup(group.id);
    const completedMatches = groupMatches.filter(m => m.status === "ended");

    const standings = new Map<string, { teamId: string; wins: number; losses: number; cups_for: number; cups_against: number; cup_differential: number }>();

    for (const team of group.teams || []) {
      const teamId = typeof team === 'string' ? team : team.id;
      standings.set(teamId, {
        teamId,
        wins: 0,
        losses: 0,
        cups_for: 0,
        cups_against: 0,
        cup_differential: 0,
      });
    }

    for (const match of completedMatches) {
      if (!match.home || !match.away) continue;

      const homeStanding = standings.get(match.home.id);
      const awayStanding = standings.get(match.away.id);

      if (homeStanding && awayStanding) {
        homeStanding.cups_for += match.home_cups;
        homeStanding.cups_against += match.away_cups;
        awayStanding.cups_for += match.away_cups;
        awayStanding.cups_against += match.home_cups;

        if (match.home_cups > match.away_cups) {
          homeStanding.wins++;
          awayStanding.losses++;
        } else {
          awayStanding.wins++;
          homeStanding.losses++;
        }

        homeStanding.cup_differential = homeStanding.cups_for - homeStanding.cups_against;
        awayStanding.cup_differential = awayStanding.cups_for - awayStanding.cups_against;
      }
    }

    const sortedStandings = Array.from(standings.values()).sort((a, b) => {
      if (b.wins !== a.wins) return b.wins - a.wins;
      if (b.cup_differential !== a.cup_differential) return b.cup_differential - a.cup_differential;
      return b.cups_for - a.cups_for;
    });

    const topTeams = sortedStandings.slice(0, groupConfig.advance_per_group);
    logger.info('Top teams from group', {
      groupId: group.id,
      topTeams: topTeams.map(t => ({ teamId: t.teamId, wins: t.wins, cupDiff: t.cup_differential }))
    });

    topTeams.forEach((standing, index) => {
      qualifiedTeams.push({
        teamId: standing.teamId,
        groupOrder: group.order,
        rank: index + 1,
      });
    });
  }

  logger.info('Qualified teams', { qualifiedTeams });

  const orderedTeamIds: string[] = [];
  const maxRank = groupConfig.advance_per_group;
  const numGroups = groupConfig.num_groups;

  const teamsByGroup: string[][] = [];
  for (let g = 0; g < numGroups; g++) {
    teamsByGroup[g] = [];
  }

  for (const qualified of qualifiedTeams) {
    teamsByGroup[qualified.groupOrder][qualified.rank - 1] = qualified.teamId;
  }

  const totalQualifiedTeams = numGroups * maxRank;
  for (let i = 0; i < totalQualifiedTeams / 2; i++) {
    const group1 = i % numGroups;
    const rankIndex1 = Math.floor(i / numGroups);

    const group2 = (i + 1) % numGroups;
    const rankIndex2 = maxRank - 1 - rankIndex1;

    const team1 = teamsByGroup[group1]?.[rankIndex1];
    const team2 = teamsByGroup[group2]?.[rankIndex2];

    if (team1) orderedTeamIds.push(team1);
    if (team2) orderedTeamIds.push(team2);
  }

  const knockoutTeamCount = orderedTeamIds.length;
  const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log2(knockoutTeamCount)));
  const wildcardsNeeded = nextPowerOf2 - knockoutTeamCount;

  if (wildcardsNeeded > 0) {
    logger.info('Wildcards needed', { knockoutTeamCount, nextPowerOf2, wildcardsNeeded });

    const allNonQualifiedTeams: Array<{ teamId: string; wins: number; losses: number; cups_for: number; cups_against: number; cup_differential: number }> = [];
    const qualifiedTeamIds = new Set(qualifiedTeams.map(t => t.teamId));

    for (const group of groups) {
      const groupMatches = await pbAdmin.getMatchesByGroup(group.id);
      const completedMatches = groupMatches.filter(m => m.status === "ended");

      const standings = new Map<string, { teamId: string; wins: number; losses: number; cups_for: number; cups_against: number; cup_differential: number }>();

      for (const team of group.teams || []) {
        const teamId = typeof team === 'string' ? team : team.id;

        if (qualifiedTeamIds.has(teamId)) continue;

        standings.set(teamId, {
          teamId,
          wins: 0,
          losses: 0,
          cups_for: 0,
          cups_against: 0,
          cup_differential: 0,
        });
      }

      for (const match of completedMatches) {
        if (!match.home || !match.away) continue;

        const homeStanding = standings.get(match.home.id);
        const awayStanding = standings.get(match.away.id);

        if (homeStanding) {
          homeStanding.cups_for += match.home_cups;
          homeStanding.cups_against += match.away_cups;
          homeStanding.cup_differential = homeStanding.cups_for - homeStanding.cups_against;

          if (match.home_cups > match.away_cups) {
            homeStanding.wins++;
          } else {
            homeStanding.losses++;
          }
        }

        if (awayStanding) {
          awayStanding.cups_for += match.away_cups;
          awayStanding.cups_against += match.home_cups;
          awayStanding.cup_differential = awayStanding.cups_for - awayStanding.cups_against;

          if (match.away_cups > match.home_cups) {
            awayStanding.wins++;
          } else {
            awayStanding.losses++;
          }
        }
      }

      allNonQualifiedTeams.push(...Array.from(standings.values()));
    }

    allNonQualifiedTeams.sort((a, b) => {
      if (b.wins !== a.wins) return b.wins - a.wins;
      if (b.cup_differential !== a.cup_differential) return b.cup_differential - a.cup_differential;
      if (b.cups_for !== a.cups_for) return b.cups_for - a.cups_for;
      return a.teamId.localeCompare(b.teamId);
    });

    const wildcardTeams = allNonQualifiedTeams.slice(0, wildcardsNeeded);
    const wildcardTeamIds = wildcardTeams.map(t => t.teamId);

    orderedTeamIds.push(...wildcardTeamIds);

    logger.info('Added wildcard teams', {
      wildcardTeams: wildcardTeams.map(t => ({
        teamId: t.teamId,
        wins: t.wins,
        cupDiff: t.cup_differential,
        cupsFor: t.cups_for
      }))
    });
  }

  logger.info('Ordered team IDs (with wildcards)', { orderedTeamIds, totalTeams: orderedTeamIds.length });

  const tournament = await pbAdmin.getTournament(tournamentId);
  const knockoutMatches = (tournament.matches || [])
    .filter((m: Match) => m.round >= 0 && m.lid >= 0)
    .sort((a: Match, b: Match) => a.lid - b.lid);

  const seedToTeamId = new Map<number, string>();
  orderedTeamIds.forEach((teamId, index) => {
    seedToTeamId.set(index + 1, teamId);
  });

  logger.info('Seed to team mapping', {
    seedToTeamId: Array.from(seedToTeamId.entries()),
    orderedTeamIds
  });

  let updatedCount = 0;
  for (const match of knockoutMatches) {
    if (match.round === 0) {
      const updates: any = {};

      if (match.home_seed) {
        const teamId = seedToTeamId.get(match.home_seed);
        logger.info('Looking up home seed', {
          matchId: match.id,
          home_seed: match.home_seed,
          teamId
        });
        if (teamId) {
          updates.home = teamId;
        }
      }

      if (match.away_seed) {
        const teamId = seedToTeamId.get(match.away_seed);
        logger.info('Looking up away seed', {
          matchId: match.id,
          away_seed: match.away_seed,
          teamId
        });
        if (teamId) {
          updates.away = teamId;
        }
      }

      if (updates.home && updates.away) {
        updates.status = "ready";
      } else if (updates.home || updates.away) {
        updates.status = "tbd";
      }

      if (Object.keys(updates).length > 0) {
        logger.info('Updating match', { matchId: match.id, updates });
        await pbAdmin.updateMatch(match.id, updates);
        updatedCount++;
      }
    }
  }

  logger.info('Updated matches', { updatedCount, totalKnockoutMatches: knockoutMatches.length });

  await pbAdmin.updateTournament(tournamentId, {
    phase: "knockout"
  });

  logger.info('Knockout bracket populated successfully', { tournamentId });
}

const teamId = (t: Match["home"] | string | undefined): string | undefined =>
  !t ? undefined : typeof t === "string" ? t : t.id;

const winnerId = (m: Match): string | undefined =>
  m.home_cups > m.away_cups ? teamId(m.home) : teamId(m.away);
const loserId = (m: Match): string | undefined =>
  m.home_cups > m.away_cups ? teamId(m.away) : teamId(m.home);

function assertValidScore(home_cups: number, away_cups: number, ot_count: number) {
  if (home_cups === away_cups) throw new Error("A match cannot end in a tie");
  if (ot_count > 0) return;
  if (home_cups !== 10 && away_cups !== 10)
    throw new Error("At least one team must have 10 cups");
  if (home_cups === 10 && away_cups === 10)
    throw new Error("Both teams cannot have 10 cups");
}

const CLEARED_RESULT = {
  home_cups: 0,
  away_cups: 0,
  ot_count: 0,
  start_time: "",
  end_time: "",
  reported_home_cups: null,
  reported_away_cups: null,
  reported_ot_count: null,
  reported_by_team: null,
  reported_by_player: null,
} as const;

function generateRecordId(): string {
  const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
  let id = "";
  for (let i = 0; i < 15; i++)
    id += alphabet[Math.floor(Math.random() * alphabet.length)];
  return id;
}

type BracketOp =
  | { kind: "updateMatch"; id: string; data: Record<string, unknown> }
  | { kind: "createMatch"; data: Record<string, unknown> }
  | { kind: "deleteMatch"; id: string }
  | { kind: "updateTournamentMatches"; tournamentId: string; matchIds: string[] };

function isBatchApiUnavailable(error: unknown): boolean {
  if (!error || typeof error !== "object") return false;
  const e = error as { status?: unknown; url?: unknown };
  const url = typeof e.url === "string" ? e.url : "";
  if (!url.includes("/api/batch")) return false;
  return e.status === 403 || e.status === 404;
}

async function commitBracketWrites(ops: BracketOp[]): Promise<void> {
  const batch = pbAdmin.createBatch();
  for (const op of ops) {
    switch (op.kind) {
      case "updateMatch":
        batch.collection("matches").update(op.id, op.data);
        break;
      case "createMatch":
        batch.collection("matches").create(op.data);
        break;
      case "deleteMatch":
        batch.collection("matches").delete(op.id);
        break;
      case "updateTournamentMatches":
        batch
          .collection("tournaments")
          .update(op.tournamentId, { matches: op.matchIds });
        break;
    }
  }

  try {
    await batch.send();
    return;
  } catch (error) {
    if (!isBatchApiUnavailable(error)) throw error;
    logger.warn(
      "PocketBase batch API is disabled; finalizing match with sequential writes (reduced atomicity). Enable Settings > Application > batch API to restore transactional bracket updates.",
    );
  }

  for (const op of ops) {
    switch (op.kind) {
      case "updateMatch":
        await pbAdmin.updateMatch(op.id, op.data as Partial<MatchInput>);
        break;
      case "createMatch":
        await pbAdmin.createMatch(op.data as unknown as MatchInput);
        break;
      case "deleteMatch":
        await pbAdmin.deleteMatch(op.id);
        break;
      case "updateTournamentMatches":
        await pbAdmin.updateTournamentMatches(op.tournamentId, op.matchIds);
        break;
    }
  }
}

async function rederiveBracket(
  tournamentId: string,
  sourceId: string,
  score: { home_cups: number; away_cups: number; ot_count: number }
): Promise<{ source: Match; downstreamReset: boolean }> {
  const all = await pbAdmin.getMatchesByTournament(tournamentId);
  const knockout = all.filter((m) => m.lid >= 0);
  const byLid = new Map<number, Match>();
  for (const m of knockout) byLid.set(m.lid, m);

  const original = all.find((m) => m.id === sourceId);
  if (!original) throw new Error("Match not found");

  const ops: BracketOp[] = [];
  let warn = false;

  const nowIso = new Date().toISOString();
  ops.push({
    kind: "updateMatch",
    id: sourceId,
    data: {
    end_time: nowIso,
    status: "ended",
    home_cups: score.home_cups,
    away_cups: score.away_cups,
    ot_count: score.ot_count,
    reported_home_cups: null,
    reported_away_cups: null,
    reported_ot_count: null,
    reported_by_team: null,
    reported_by_player: null,
    },
  });
  const source: Match = {
    ...original,
    status: "ended",
    home_cups: score.home_cups,
    away_cups: score.away_cups,
    ot_count: score.ot_count,
    end_time: nowIso,
    reported_home_cups: undefined,
    reported_away_cups: undefined,
    reported_ot_count: undefined,
    reported_by_team: undefined,
    reported_by_player: undefined,
  };
  byLid.set(source.lid, source);

  const resolveFeeder = (lid: number, fromLoser: boolean): string | undefined => {
    const f = byLid.get(lid);
    if (!f || f.bye || f.status !== "ended") return undefined;
    return fromLoser ? loserId(f) : winnerId(f);
  };

  const hasLosers = knockout.some((m) => m.is_losers_bracket);
  const resetMatch = knockout.find((m) => m.reset);
  const grandFinal = hasLosers
    ? knockout
        .filter((m) => !m.reset && !m.bye)
        .reduce<Match | undefined>(
          (hi, cur) => (!hi || cur.lid > hi.lid ? cur : hi),
          undefined
        )
    : undefined;

  const ordered = knockout
    .filter((m) => !m.bye && !m.reset)
    .sort((a, b) => a.lid - b.lid);

  for (const m of ordered) {
    if (m.id === sourceId) continue;

    const homeFed = m.home_from_lid >= 0;
    const awayFed = m.away_from_lid >= 0;
    if (!homeFed && !awayFed) continue;

    const curHome = teamId(m.home);
    const curAway = teamId(m.away);
    const expHome = homeFed ? resolveFeeder(m.home_from_lid, m.home_from_loser) : curHome;
    const expAway = awayFed ? resolveFeeder(m.away_from_lid, m.away_from_loser) : curAway;
    const expStatus = expHome && expAway ? "ready" : "tbd";

    const played = m.status === "started" || m.status === "ended";
    const changed =
      (homeFed && expHome !== curHome) || (awayFed && expAway !== curAway);

    if (played && changed) {
      ops.push({
        kind: "updateMatch",
        id: m.id,
        data: {
          home: expHome ?? null,
          away: expAway ?? null,
          status: expStatus,
          ...CLEARED_RESULT,
        },
      });
      byLid.set(m.lid, { ...m, status: expStatus });
      warn = true;
    } else if (!played && (changed || m.status !== expStatus)) {
      ops.push({
        kind: "updateMatch",
        id: m.id,
        data: {
          home: expHome ?? null,
          away: expAway ?? null,
          status: expStatus,
        },
      });
      byLid.set(m.lid, { ...m, status: expStatus });
    }
  }

  if (grandFinal) {
    const gf = byLid.get(grandFinal.lid)!;
    const gfEnded = gf.status === "ended";
    const resetNeeded = gfEnded && winnerId(gf) === teamId(gf.away);

    if (resetNeeded) {
      const expHome = winnerId(gf);
      const expAway = loserId(gf);

      if (resetMatch && resetMatch.id !== sourceId) {
        const played =
          resetMatch.status === "started" || resetMatch.status === "ended";
        const changed =
          teamId(resetMatch.home) !== expHome || teamId(resetMatch.away) !== expAway;
        if (played && changed) {
          ops.push({
            kind: "updateMatch",
            id: resetMatch.id,
            data: {
              home: expHome ?? null,
              away: expAway ?? null,
              status: "ready",
              ...CLEARED_RESULT,
            },
          });
          warn = true;
        } else if (!played && (changed || resetMatch.status !== "ready")) {
          ops.push({
            kind: "updateMatch",
            id: resetMatch.id,
            data: {
              home: expHome ?? null,
              away: expAway ?? null,
              status: "ready",
            },
          });
        }
      } else if (!resetMatch) {
        const newId = generateRecordId();
        ops.push({
          kind: "createMatch",
          data: {
            id: newId,
            lid: gf.lid + 1,
            order: gf.order + 1,
            round: gf.round + 1,
            reset: true,
            bye: false,
            home_cups: 0,
            away_cups: 0,
            ot_count: 0,
            home_from_lid: gf.lid,
            away_from_lid: gf.lid,
            home_from_loser: false,
            away_from_loser: true,
            is_losers_bracket: false,
            status: "ready",
            tournament: tournamentId,
            home: expHome ?? undefined,
            away: expAway ?? undefined,
          },
        });
        ops.push({
          kind: "updateTournamentMatches",
          tournamentId,
          matchIds: [...all.map((m) => m.id), newId],
        });
      }
    } else if (resetMatch && resetMatch.id !== sourceId) {
      if (resetMatch.status === "started" || resetMatch.status === "ended") {
        warn = true;
      }
      ops.push({ kind: "deleteMatch", id: resetMatch.id });
    }
  }

  await commitBracketWrites(ops);

  return { source, downstreamReset: warn };
}

async function finalizeMatch(
  matchId: string,
  { home_cups, away_cups, ot_count }: { home_cups: number; away_cups: number; ot_count: number }
): Promise<{ match: Match; downstreamReset: boolean; groupEditAfterKnockout: boolean }> {
  assertValidScore(home_cups, away_cups, ot_count);

  const existing = await pbAdmin.getMatch(matchId);
  if (!existing) throw new Error("Match not found");
  const tournamentId = existing.tournament.id;

  if (existing.lid === -1) {
    const source = await pbAdmin.updateMatch(matchId, {
      end_time: new Date().toISOString(),
      status: "ended",
      home_cups,
      away_cups,
      ot_count,
      reported_home_cups: null,
      reported_away_cups: null,
      reported_ot_count: null,
      reported_by_team: null,
      reported_by_player: null,
    });

    const all = await pbAdmin.getMatchesByTournament(tournamentId);
    const groupEditAfterKnockout = all.some(
      (m) => m.round >= 0 && (teamId(m.home) || teamId(m.away))
    );

    emitServerEvent({ type: "match", matchId: source.id, tournamentId });
    return { match: source, downstreamReset: false, groupEditAfterKnockout };
  }

  const { source, downstreamReset } = await rederiveBracket(tournamentId, matchId, {
    home_cups,
    away_cups,
    ot_count,
  });

  emitServerEvent({ type: "match", matchId: source.id, tournamentId });
  return { match: source, downstreamReset, groupEditAfterKnockout: false };
}

const endMatchSchema = z.object({
  matchId: z.string(),
  home_cups: z.number(),
  away_cups: z.number(),
  ot_count: z.number(),
});
export const endMatch = createServerFn()
  .validator(endMatchSchema)
  .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
  .handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) =>
    toServerResult(async () => {
      logger.info("Ending match", matchId);

      const match = await pbAdmin.getMatch(matchId);
      if (!match) {
        throw new Error("Match not found");
      }

      return finalizeMatch(matchId, { home_cups, away_cups, ot_count });
    })
  );

const teamPlayerIds = (team: Match["home"]): string[] =>
  team && typeof team !== "string" ? (team.players ?? []).map((p) => p.id) : [];

const reportScoreSchema = z.object({
  matchId: z.string(),
  home_cups: z.number(),
  away_cups: z.number(),
  ot_count: z.number(),
});

export const reportMatchScore = createServerFn()
  .validator(reportScoreSchema)
  .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
  .handler(async ({ data: { matchId, home_cups, away_cups, ot_count }, context }) =>
    toServerResult(async () => {
      const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
      if (!player?.id) throw new Error("Player not found");

      const match = await pbAdmin.getMatch(matchId);
      if (!match) throw new Error("Match not found");
      if (match.status !== "started")
        throw new Error("Scores can only be reported once the match has started");

      const onHome = teamPlayerIds(match.home).includes(player.id);
      const onAway = teamPlayerIds(match.away).includes(player.id);
      if (!onHome && !onAway)
        throw new Error("You are not a player in this match");
      if (onHome && onAway)
        throw new Error(
          "You are on both teams in this match and cannot report or confirm its score"
        );

      assertValidScore(home_cups, away_cups, ot_count);

      const callerTeamId = onHome ? teamId(match.home) : teamId(match.away);

      const hasPending =
        match.reported_by_team != null &&
        match.reported_home_cups != null &&
        match.reported_away_cups != null;
      const fromOppositeTeam =
        hasPending && match.reported_by_team !== callerTeamId;
      const identical =
        hasPending &&
        match.reported_home_cups === home_cups &&
        match.reported_away_cups === away_cups &&
        (match.reported_ot_count ?? 0) === ot_count;

      if (fromOppositeTeam && identical) {
        const result = await finalizeMatch(matchId, { home_cups, away_cups, ot_count });
        return {
          success: true as const,
          finalized: true as const,
          downstreamReset: result.downstreamReset,
          groupEditAfterKnockout: result.groupEditAfterKnockout,
        };
      }

      await pbAdmin.updateMatch(matchId, {
        reported_home_cups: home_cups,
        reported_away_cups: away_cups,
        reported_ot_count: ot_count,
        reported_by_team: callerTeamId,
        reported_by_player: player.id,
      });

      emitServerEvent({ type: "match", matchId, tournamentId: match.tournament.id });
      return { success: true as const, finalized: false as const };
    })
  );

const matchIdSchema = z.object({ matchId: z.string() });

export const confirmMatchScore = createServerFn()
  .validator(matchIdSchema)
  .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
  .handler(async ({ data: { matchId }, context }) =>
    toServerResult(async () => {
      const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
      if (!player?.id) throw new Error("Player not found");

      const match = await pbAdmin.getMatch(matchId);
      if (!match) throw new Error("Match not found");
      if (match.status !== "started")
        throw new Error("Match is not in progress");
      if (!match.reported_by_team)
        throw new Error("No score has been reported yet");
      if (match.reported_home_cups == null || match.reported_away_cups == null)
        throw new Error("The reported score is incomplete");

      const onHome = teamPlayerIds(match.home).includes(player.id);
      const onAway = teamPlayerIds(match.away).includes(player.id);
      if (onHome && onAway)
        throw new Error(
          "You are on both teams in this match and cannot report or confirm its score"
        );

      const homeId = teamId(match.home);
      const reportedByHome = match.reported_by_team === homeId;
      const opposingTeam = reportedByHome ? match.away : match.home;

      if (!teamPlayerIds(opposingTeam).includes(player.id))
        throw new Error("Only a player on the other team can confirm this score");

      return finalizeMatch(matchId, {
        home_cups: match.reported_home_cups,
        away_cups: match.reported_away_cups,
        ot_count: match.reported_ot_count ?? 0,
      });
    })
  );

export const clearMatchReport = createServerFn()
  .validator(matchIdSchema)
  .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
  .handler(async ({ data: { matchId }, context }) =>
    toServerResult(async () => {
      const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
      if (!player?.id) throw new Error("Player not found");

      const match = await pbAdmin.getMatch(matchId);
      if (!match) throw new Error("Match not found");

      const onHome = teamPlayerIds(match.home).includes(player.id);
      const onAway = teamPlayerIds(match.away).includes(player.id);
      if (!onHome && !onAway)
        throw new Error("You are not a player in this match");

      await pbAdmin.updateMatch(matchId, {
        reported_home_cups: null,
        reported_away_cups: null,
        reported_ot_count: null,
        reported_by_team: null,
        reported_by_player: null,
      });

      emitServerEvent({ type: "match", matchId, tournamentId: match.tournament.id });
      return { success: true };
    })
  );

const toggleReactionSchema = z.object({
  matchId: z.string(),
  emoji: z.string(),
});

export const toggleMatchReaction = createServerFn()
  .validator(toggleReactionSchema)
  .middleware([superTokensFunctionMiddleware])
  .handler(async ({ data: { matchId, emoji }, context }) =>
    toServerResult(async () => {
      const user = await pbAdmin.getPlayerByAuthId(context.userAuthId);
      const userId = user?.id;
      if (!userId) return;

      const match = await pbAdmin.getMatch(matchId);
      if (!match) {
        throw new Error("Match not found");
      }

      const existingReaction = await pbAdmin.getUserReaction(matchId, userId, emoji);
      if (existingReaction) {
        await pbAdmin.deleteReaction(existingReaction.id);
        logger.info("Removed reaction", { matchId, emoji, userId });
      } else {
        await pbAdmin.createReaction(matchId, userId, emoji);
        logger.info("Added reaction", { matchId, emoji, userId });
      }

      const all = await pbAdmin.getReactionsForMatch(matchId);

      const reactionsByEmoji = all.reduce((acc, reaction) => {
        const emoji = reaction.emoji;
        if (!acc[emoji]) {
          acc[emoji] = {
            emoji,
            count: 0,
            players: [],
          };
        }
        acc[emoji].count++;
        acc[emoji].players.push({
          id: reaction?.player?.id,
          first_name: reaction.player?.first_name,
          last_name: reaction.player?.last_name,
        });
        return acc;
      }, {} as Record<string, any>);

      const reactions = Object.values(reactionsByEmoji);

      emitServerEvent({
        type: "reaction",
        matchId,
        reactions,
      });

      return reactions as Reaction[]
    })
  );

export interface Reaction {
  emoji: string;
  count: number;
  players: PlayerInfo[];
}
export const getMatchReactions = createServerFn()
  .validator(z.string())
  .middleware([superTokensFunctionMiddleware])
  .handler(async ({ data: matchId, context }) =>
    toServerResult(async () => {
      const match = await pbAdmin.getMatch(matchId);
      if (!match) {
        throw new Error("Match not found");
      }

      const all = await pbAdmin.getReactionsForMatch(matchId);

      const reactionsByEmoji = all.reduce((acc, reaction) => {
        const emoji = reaction.emoji;
        if (!acc[emoji]) {
          acc[emoji] = {
            emoji,
            count: 0,
            players: [],
          };
        }
        acc[emoji].count++;
        acc[emoji].players.push({
          id: reaction?.player?.id,
          first_name: reaction.player?.first_name,
          last_name: reaction.player?.last_name,
        });
        return acc;
      }, {} as Record<string, any>);

      const reactions = Object.values(reactionsByEmoji);

      return reactions as Reaction[]
    })
  );

const matchesBetweenPlayersSchema = z.object({
  player1Id: z.string(),
  player2Id: z.string(),
});

export const getMatchesBetweenPlayers = createServerFn()
  .validator(matchesBetweenPlayersSchema)
  .middleware([superTokensFunctionMiddleware])
  .handler(async ({ data: { player1Id, player2Id } }) =>
    toServerResult(async () => {
      logger.info("Getting matches between players", { player1Id, player2Id });
      const matches = await pbAdmin.getMatchesBetweenPlayers(player1Id, player2Id);
      return matches;
    })
  );

const matchesBetweenTeamsSchema = z.object({
  team1Id: z.string(),
  team2Id: z.string(),
});

export const getMatchesBetweenTeams = createServerFn()
  .validator(matchesBetweenTeamsSchema)
  .middleware([superTokensFunctionMiddleware])
  .handler(async ({ data: { team1Id, team2Id } }) =>
    toServerResult(async () => {
      logger.info("Getting matches between teams", { team1Id, team2Id });
      const matches = await pbAdmin.getMatchesBetweenTeams(team1Id, team2Id);
      return matches;
    })
  );
