import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
import { pbAdmin } from "@/lib/pocketbase/client";
import { logger } from "@/lib/logger";
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
import { superTokensFunctionMiddleware } from "@/utils/supertokens";
import { serverFnLoggingMiddleware } from "@/utils/activities";
import { Tournament } from "@/features/tournaments/types";
import {
  MyPrediction,
  Prediction,
  PredictionLeaderboardEntry,
  PredictionsLeaderboard,
} from "./types";
import {
  computePredictionScore,
  getPickableMatches,
  isPredictionComplete,
  isPredictionLocked,
  isTournamentPredictable,
} from "./utils";

const getTournamentOrThrow = async (tournamentId: string): Promise<Tournament> => {
  const tournament = await pbAdmin.getTournament(tournamentId);
  if (!tournament) {
    throw new Error("Tournament not found");
  }
  return tournament;
};

export const getMyPrediction = createServerFn()
  .validator(z.string())
  .middleware([superTokensFunctionMiddleware])
  .handler(async ({ data: tournamentId, context }) =>
    toServerResult(async (): Promise<MyPrediction> => {
      const tournament = await getTournamentOrThrow(tournamentId);
      const matches = tournament.matches || [];

      const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
      const prediction = player
        ? await pbAdmin.getPrediction(tournamentId, player.id)
        : null;

      return {
        prediction,
        locked: isPredictionLocked(matches),
        eligible: isTournamentPredictable(tournament),
      };
    })
  );

const submitPredictionSchema = z.object({
  tournamentId: z.string(),
  picks: z.record(z.string(), z.string()),
});

export const submitPrediction = createServerFn()
  .validator(submitPredictionSchema)
  .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
  .handler(async ({ data: { tournamentId, picks }, context }) =>
    toServerResult(async (): Promise<Prediction> => {
      const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
      if (!player) {
        throw new Error("Player not found");
      }

      const tournament = await getTournamentOrThrow(tournamentId);
      const matches = tournament.matches || [];

      if (!isTournamentPredictable(tournament)) {
        throw new Error("Predictions are not available for this tournament");
      }

      if (isPredictionLocked(matches)) {
        throw new Error("Predictions are locked — the tournament has started");
      }

      const pickableLids = new Set(
        getPickableMatches(matches, picks).map((match) => String(match.lid))
      );
      const pickKeys = Object.keys(picks);
      if (
        pickKeys.length !== pickableLids.size ||
        pickKeys.some((lid) => !pickableLids.has(lid))
      ) {
        throw new Error("Prediction must include a pick for every match");
      }

      if (!isPredictionComplete(matches, picks)) {
        throw new Error("Prediction contains invalid picks");
      }

      const prediction = await pbAdmin.upsertPrediction(
        tournamentId,
        player.id,
        picks
      );

      logger.info("Prediction submitted", {
        tournamentId,
        playerId: player.id,
        pickCount: pickKeys.length,
      });

      return prediction;
    })
  );

export const getPredictionsLeaderboard = createServerFn()
  .validator(z.string())
  .middleware([superTokensFunctionMiddleware])
  .handler(async ({ data: tournamentId }) =>
    toServerResult(async (): Promise<PredictionsLeaderboard> => {
      const tournament = await getTournamentOrThrow(tournamentId);
      const matches = tournament.matches || [];
      const locked = isPredictionLocked(matches);

      const predictions = await pbAdmin.getPredictionsForTournament(tournamentId);
      const submitters = predictions.map((prediction) => prediction.player);

      if (!locked) {
        return { locked, count: predictions.length, entries: [], submitters };
      }

      const entries: PredictionLeaderboardEntry[] = predictions.map(
        (prediction) => {
          const score = computePredictionScore(matches, prediction.picks);
          const championPick = tournament.teams?.find(
            (team) => team.id === score.predictedChampionId
          );

          return {
            player: prediction.player,
            points: score.points,
            correct: score.correct,
            total: score.total,
            championPick,
            championCorrect:
              !!score.predictedChampionId &&
              score.predictedChampionId === tournament.first_place?.id,
            updated: prediction.updated,
          };
        }
      );

      entries.sort(
        (a, b) =>
          b.points - a.points ||
          Number(b.championCorrect) - Number(a.championCorrect) ||
          a.updated.localeCompare(b.updated) ||
          (a.player.first_name ?? "").localeCompare(b.player.first_name ?? "")
      );

      return { locked, count: entries.length, entries, submitters };
    })
  );

const playerPredictionSchema = z.object({
  tournamentId: z.string(),
  playerId: z.string(),
});

export const getPlayerPrediction = createServerFn()
  .validator(playerPredictionSchema)
  .middleware([superTokensFunctionMiddleware])
  .handler(async ({ data: { tournamentId, playerId }, context }) =>
    toServerResult(async (): Promise<Prediction | null> => {
      const tournament = await getTournamentOrThrow(tournamentId);

      if (!isPredictionLocked(tournament.matches || [])) {
        const me = await pbAdmin.getPlayerByAuthId(context.userAuthId);
        if (me?.id !== playerId) {
          throw new Error("Predictions are private until the tournament starts");
        }
      }

      return pbAdmin.getPrediction(tournamentId, playerId);
    })
  );
