import type { WebSocket } from "ws";
import type { User } from "better-auth";
import type { WsGameAnswer, WsGameReady } from "@lila/shared";
import { finishGame, getLobbyByCodeWithPlayers } from "@lila/db";
import { broadcastToLobby, getConnections } from "../connections.js";
import { lobbyGameStore, timers } from "../gameState.js";
import { NotFoundError, ConflictError } from "../../errors/AppError.js";

export const handleGameAnswer = async (
  _ws: WebSocket,
  msg: WsGameAnswer,
  user: User,
): Promise<void> => {
  const state = await lobbyGameStore.get(msg.lobbyId);
  if (!state) {
    throw new NotFoundError("Game not found");
  }

  const currentQuestion = state.questions[state.currentIndex];
  if (!currentQuestion) {
    throw new ConflictError("No active question");
  }

  // Reject stale answers
  if (currentQuestion.questionId !== msg.questionId) {
    throw new ConflictError("Answer is for wrong question");
  }

  // Reject duplicate answers
  if (state.playerAnswers.has(user.id)) {
    throw new ConflictError("Already answered this question");
  }

  // Store answer
  state.playerAnswers.set(user.id, msg.selectedOptionId);
  await lobbyGameStore.set(msg.lobbyId, state);

  // Check if all connected players have answered
  const connected = getConnections(msg.lobbyId);
  const allAnswered = [...connected.keys()].every((userId) =>
    state.playerAnswers.has(userId),
  );

  if (allAnswered) {
    // Clear timer — no need to wait
    const timer = timers.get(msg.lobbyId);
    if (timer) {
      clearTimeout(timer);
      timers.delete(msg.lobbyId);
    }
    await resolveRound(msg.lobbyId, state.currentIndex, state.questions.length);
  }
};

export const handleGameReady = async (
  ws: WebSocket,
  msg: WsGameReady,
  _user: User,
): Promise<void> => {
  const state = await lobbyGameStore.get(msg.lobbyId);
  if (!state) throw new NotFoundError("Game not found");

  const currentQuestion = state.questions[state.currentIndex];
  if (!currentQuestion) throw new NotFoundError("No active question");

  ws.send(
    JSON.stringify({
      type: "game:question",
      question: {
        questionId: currentQuestion.questionId,
        prompt: currentQuestion.prompt,
        gloss: currentQuestion.gloss,
        options: currentQuestion.options,
      },
      questionNumber: state.currentIndex + 1,
      totalQuestions: state.questions.length,
    }),
  );
};

export const resolveRound = async (
  lobbyId: string,
  questionIndex: number,
  totalQuestions: number,
): Promise<void> => {
  const state = await lobbyGameStore.get(lobbyId);
  if (!state) return; // lobby was deleted mid-round, nothing to do

  const currentQuestion = state.questions[questionIndex];
  if (!currentQuestion) return;

  // Fill null for any players who didn't answer (timed out)
  const connected = getConnections(lobbyId);
  for (const userId of connected.keys()) {
    if (!state.playerAnswers.has(userId)) {
      state.playerAnswers.set(userId, null);
    }
  }

  // Evaluate answers and update scores
  const results: {
    userId: string;
    selectedOptionId: number | null;
    isCorrect: boolean;
  }[] = [];

  for (const [userId, selectedOptionId] of state.playerAnswers) {
    const isCorrect =
      selectedOptionId !== null &&
      selectedOptionId === currentQuestion.correctOptionId;
    if (isCorrect) {
      state.scores.set(userId, (state.scores.get(userId) ?? 0) + 1);
    }
    results.push({ userId, selectedOptionId, isCorrect });
  }

  // Build updated players array for broadcast
  const players = [...state.scores.entries()].map(([userId, score]) => ({
    userId,
    score,
    lobbyId,
    user: { id: userId, name: userId }, // name resolved below
  }));

  // Resolve user names from DB
  const lobby = await getLobbyByCodeWithPlayers(state.code);

  const namedPlayers = players.map((p) => {
    const dbPlayer = lobby?.players.find((dp) => dp.userId === p.userId);
    return {
      ...p,
      user: { id: p.userId, name: dbPlayer?.user.name ?? p.userId },
    };
  });

  // Broadcast answer result
  broadcastToLobby(lobbyId, {
    type: "game:answer_result",
    correctOptionId: currentQuestion.correctOptionId,
    results,
    players: namedPlayers,
  });

  // Save updated state
  state.playerAnswers = new Map();
  state.currentIndex = questionIndex + 1;
  await lobbyGameStore.set(lobbyId, state);

  const isLastRound = questionIndex + 1 >= totalQuestions;

  if (isLastRound) {
    await endGame(lobbyId, state);
  } else {
    // Wait 3s then broadcast next question
    setTimeout(() => {
      void (async () => {
        const fresh = await lobbyGameStore.get(lobbyId);
        if (!fresh) return;
        const nextQuestion = fresh.questions[fresh.currentIndex];
        if (!nextQuestion) return;
        broadcastToLobby(lobbyId, {
          type: "game:question",
          question: {
            questionId: nextQuestion.questionId,
            prompt: nextQuestion.prompt,
            gloss: nextQuestion.gloss,
            options: nextQuestion.options,
          },
          questionNumber: fresh.currentIndex + 1,
          totalQuestions,
        });
        // Restart timer for next round
        const timer = setTimeout(() => {
          void resolveRound(lobbyId, fresh.currentIndex, totalQuestions);
        }, 15000);
        timers.set(lobbyId, timer);
      })();
    }, 3000);
  }
};

const endGame = async (
  lobbyId: string,
  state: Awaited<ReturnType<typeof lobbyGameStore.get>> & {},
): Promise<void> => {
  // Persist final scores to DB
  await finishGame(lobbyId, state.scores);

  // Determine winners (handle ties)
  const maxScore = Math.max(...state.scores.values());
  const winnerIds = [...state.scores.entries()]
    .filter(([, score]) => score === maxScore)
    .map(([userId]) => userId);

  // Build final players array
  const lobby = await getLobbyByCodeWithPlayers(state.code);

  const players = [...state.scores.entries()].map(([userId, score]) => {
    const dbPlayer = lobby?.players.find((p) => p.userId === userId);
    return {
      lobbyId,
      userId,
      score,
      user: { id: userId, name: dbPlayer?.user.name ?? userId },
    };
  });

  broadcastToLobby(lobbyId, { type: "game:finished", players, winnerIds });

  // Clean up game state
  await lobbyGameStore.delete(lobbyId);
  timers.delete(lobbyId);
};
