<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\DartThrow;
use App\Entity\Game;
use App\Entity\GamePlayer;
use App\Entity\User;
use App\Enum\GameModeType;
use App\Enum\GameStatus;
use App\Enum\ThrowRule;
use App\Repository\GameModeRepository;
use App\Repository\GameRepository;
use App\Repository\TurnRepository;
use App\Service\GameEngine;
use App\Service\GameModeBootstrapper;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/api/games')]
class ApiGameController extends AbstractController
{
    public function __construct(
        private readonly GameRepository $gameRepository,
        private readonly GameModeRepository $gameModeRepository,
        private readonly TurnRepository $turnRepository,
        private readonly EntityManagerInterface $entityManager,
        private readonly GameEngine $gameEngine,
        private readonly GameModeBootstrapper $bootstrapper,
    ) {
    }

    #[Route('', name: 'api_game_list', methods: ['GET'])]
    public function list(): JsonResponse
    {
        $this->bootstrapper->ensureDefaults();
        $user = $this->getAppUser();

        $serializeGameSummary = static fn (Game $game): array => [
            'id' => $game->getId(),
            'status' => $game->getStatus()->value,
            'mode' => [
                'name' => $game->getMode()->getName(),
            ],
            'createdAt' => $game->getCreatedAt()->format(\DateTimeInterface::ATOM),
            'startedAt' => $game->getStartedAt()?->format(\DateTimeInterface::ATOM),
            'finishedAt' => $game->getFinishedAt()?->format(\DateTimeInterface::ATOM),
            'updatedAt' => $game->getUpdatedAt()->format(\DateTimeInterface::ATOM),
        ];

        return $this->json([
            'running' => array_map($serializeGameSummary, $this->gameRepository->findByOwnerAndStatus($user, GameStatus::RUNNING)),
            'aborted' => array_map($serializeGameSummary, $this->gameRepository->findByOwnerAndStatus($user, GameStatus::ABORTED)),
            'finished' => array_map($serializeGameSummary, $this->gameRepository->findByOwnerAndStatus($user, GameStatus::FINISHED)),
        ]);
    }

    #[Route('', name: 'api_game_create', methods: ['POST'])]
    public function create(Request $request): JsonResponse
    {
        $this->bootstrapper->ensureDefaults();
        $user = $this->getAppUser();

        try {
            $payload = $this->decodeJsonRequest($request);
        } catch (\InvalidArgumentException $exception) {
            return $this->jsonError($exception->getMessage(), Response::HTTP_BAD_REQUEST);
        }

        $modeKey = trim((string) ($payload['modeKey'] ?? ''));
        $mode = $this->gameModeRepository->findOneByKey($modeKey);

        if ($mode === null) {
            return $this->jsonError('A valid modeKey is required.', Response::HTTP_BAD_REQUEST);
        }

        $playersRaw = $payload['players'] ?? null;
        if (!\is_array($playersRaw)) {
            return $this->jsonError('Players must be provided as an array.', Response::HTTP_BAD_REQUEST);
        }

        $players = $this->extractPlayers($playersRaw);
        if ($players === []) {
            return $this->jsonError('At least one player is required.', Response::HTTP_BAD_REQUEST);
        }

        $inRule = ThrowRule::tryFrom((string) ($payload['inRule'] ?? ThrowRule::ANY->value)) ?? ThrowRule::ANY;
        $outRule = ThrowRule::tryFrom((string) ($payload['outRule'] ?? ThrowRule::ANY->value)) ?? ThrowRule::ANY;
        if ($mode->getType() === GameModeType::ATW) {
            $inRule = ThrowRule::ANY;
            $outRule = ThrowRule::ANY;
        }

        $game = new Game($user, $mode, $inRule, $outRule);
        foreach ($players as $index => $playerData) {
            $game->addPlayer(new GamePlayer(
                name: $playerData['name'],
                position: $index,
                startScore: $mode->getStartScore(),
            ));
        }

        $this->entityManager->persist($game);
        $this->entityManager->flush();

        return $this->json([
            'game' => $this->serializeGame($game, $user),
        ], Response::HTTP_CREATED);
    }

    #[Route('/{id}', name: 'api_game_show', methods: ['GET'])]
    public function show(string $id): JsonResponse
    {
        $user = $this->getUser();
        $viewer = $user instanceof User ? $user : null;
        $game = $this->requireReadableGame($id);

        return $this->json([
            'game' => $this->serializeGame($game, $viewer),
        ]);
    }

    #[Route('/{id}/start', name: 'api_game_start', methods: ['POST'])]
    public function start(string $id): JsonResponse
    {
        $game = $this->requireOwnedGame($id);
        $user = $this->getAppUser();

        try {
            if ($game->getStatus() === GameStatus::DRAFT) {
                $this->gameEngine->startGame($game);
            }
        } catch (\Throwable $exception) {
            return $this->jsonError($exception->getMessage(), Response::HTTP_BAD_REQUEST);
        }

        return $this->json([
            'game' => $this->serializeGame($game, $user),
        ]);
    }

    #[Route('/{id}/throws', name: 'api_game_throw', methods: ['POST'])]
    public function throw(string $id, Request $request): JsonResponse
    {
        $game = $this->requireOwnedGame($id);
        $user = $this->getAppUser();

        try {
            $payload = $this->decodeJsonRequest($request);
            $segmentRaw = $payload['segment'] ?? null;
            $multiplierRaw = $payload['multiplier'] ?? null;

            if ($segmentRaw === null) {
                throw new \InvalidArgumentException('Segment is required.');
            }

            $segmentValue = \is_string($segmentRaw) ? strtoupper(trim($segmentRaw)) : (string) $segmentRaw;
            $multiplierValue = (int) ($multiplierRaw ?? 0);

            if ($segmentValue === 'BULL') {
                $this->gameEngine->commitThrow($game, 'BULL', $multiplierValue);
            } elseif ($segmentValue === 'OUT') {
                $this->gameEngine->commitThrow($game, 'OUT', 0);
            } else {
                $this->gameEngine->commitThrow($game, (int) $segmentValue, $multiplierValue);
            }
        } catch (\Throwable $exception) {
            return $this->jsonError($exception->getMessage(), Response::HTTP_BAD_REQUEST);
        }

        return $this->json([
            'game' => $this->serializeGame($game, $user),
        ]);
    }

    #[Route('/{id}/undo', name: 'api_game_undo', methods: ['POST'])]
    public function undo(string $id): JsonResponse
    {
        $game = $this->requireOwnedGame($id);
        $user = $this->getAppUser();

        try {
            $this->gameEngine->undoLastThrow($game);
        } catch (\Throwable $exception) {
            return $this->jsonError($exception->getMessage(), Response::HTTP_BAD_REQUEST);
        }

        return $this->json([
            'game' => $this->serializeGame($game, $user),
        ]);
    }

    #[Route('/{id}/finish', name: 'api_game_finish', methods: ['POST'])]
    public function finish(string $id): JsonResponse
    {
        $game = $this->requireOwnedGame($id);
        $user = $this->getAppUser();

        if ($game->getStatus() === GameStatus::RUNNING) {
            $game->setStatus(GameStatus::ABORTED)
                ->setFinishedAt(new \DateTimeImmutable());
            $this->entityManager->flush();
        }

        return $this->json([
            'game' => $this->serializeGame($game, $user),
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    private function decodeJsonRequest(Request $request): array
    {
        try {
            $payload = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
        } catch (\JsonException) {
            throw new \InvalidArgumentException('Invalid JSON payload.');
        }

        if (!\is_array($payload)) {
            throw new \InvalidArgumentException('Invalid JSON payload.');
        }

        return $payload;
    }

    /**
     * @param array<int|string, mixed> $playersRaw
     *
     * @return list<array{name: string, position: int}>
     */
    private function extractPlayers(array $playersRaw): array
    {
        $rows = [];

        foreach ($playersRaw as $index => $row) {
            if (!\is_array($row)) {
                continue;
            }

            $name = trim((string) ($row['name'] ?? ''));
            if ($name === '') {
                continue;
            }

            $rawPosition = $row['position'] ?? $index;
            $position = is_numeric($rawPosition) ? (int) $rawPosition : (int) $index;

            $rows[] = [
                'name' => $name,
                'position' => $position,
            ];
        }

        usort($rows, static fn (array $a, array $b): int => $a['position'] <=> $b['position']);

        return $rows;
    }

    private function requireOwnedGame(string $id): Game
    {
        $game = $this->gameRepository->findOwnedWithPlayers($id, $this->getAppUser());
        if (!$game instanceof Game) {
            throw $this->createNotFoundException('Game not found');
        }

        return $game;
    }

    private function requireReadableGame(string $id): Game
    {
        $game = $this->gameRepository->findWithPlayers($id);
        if (!$game instanceof Game) {
            throw $this->createNotFoundException('Game not found');
        }

        return $game;
    }

    private function getAppUser(): User
    {
        $user = $this->getUser();
        if (!$user instanceof User) {
            throw new AccessDeniedHttpException();
        }

        return $user;
    }

    private function jsonError(string $message, int $statusCode): JsonResponse
    {
        return $this->json(['message' => $message], $statusCode);
    }

    /**
     * @return array<string, mixed>
     */
    private function serializeGame(Game $game, ?User $viewer = null): array
    {
        $players = $game->getPlayers()->toArray();
        usort($players, static fn (GamePlayer $a, GamePlayer $b): int => $a->getPosition() <=> $b->getPosition());

        $isOwner = $viewer instanceof User && $game->getOwner()->getId() === $viewer->getId();

        $throwHistoryByPlayer = [];
        foreach ($players as $player) {
            $throwHistoryByPlayer[$player->getId()] = [];
        }

        $activePlayerId = null;
        if ($game->getStatus() === GameStatus::RUNNING && $players !== []) {
            $activePlayerId = $this->gameEngine->getActivePlayer($game)->getId();
        }

        $openTurn = $this->turnRepository->findOpenTurn($game);
        $openTurnPlayerId = $openTurn?->getPlayer()->getId();
        $openTurnThrows = $openTurn !== null ? $openTurn->getThrows()->toArray() : [];
        usort($openTurnThrows, static fn (DartThrow $a, DartThrow $b): int => $a->getDartIndex() <=> $b->getDartIndex());

        foreach ($players as $player) {
            $playerId = $player->getId();
            if ($playerId === $activePlayerId && $playerId === $openTurnPlayerId) {
                $throwHistoryByPlayer[$playerId] = array_slice($openTurnThrows, -3);
                continue;
            }

            $lastCompletedTurn = $this->turnRepository->findLastCompletedTurnForPlayer($game, $player);
            if ($lastCompletedTurn === null) {
                $throwHistoryByPlayer[$playerId] = [];
                continue;
            }

            $completedTurnThrows = $lastCompletedTurn->getThrows()->toArray();
            usort($completedTurnThrows, static fn (DartThrow $a, DartThrow $b): int => $a->getDartIndex() <=> $b->getDartIndex());
            $throwHistoryByPlayer[$playerId] = array_slice($completedTurnThrows, -3);
        }

        return [
            'id' => $game->getId(),
            'isOwner' => $isOwner,
            'status' => $game->getStatus()->value,
            'currentPlayerIndex' => $game->getCurrentPlayerIndex(),
            'currentTurnNumber' => $game->getCurrentTurnNumber(),
            'currentDartInTurn' => $game->getCurrentDartInTurn(),
            'inRule' => $game->getInRule()->value,
            'outRule' => $game->getOutRule()->value,
            'activePlayerId' => $activePlayerId,
            'mode' => [
                'id' => $game->getMode()->getId(),
                'key' => $game->getMode()->getKey(),
                'name' => $game->getMode()->getName(),
                'startScore' => $game->getMode()->getStartScore(),
                'type' => $game->getMode()->getType()->value,
            ],
            'players' => array_map(
                fn (GamePlayer $player): array => [
                    'id' => $player->getId(),
                    'name' => $player->getName(),
                    'position' => $player->getPosition(),
                    'currentScore' => $player->getCurrentScore(),
                    'isWinner' => $player->isWinner(),
                    'hasCheckedIn' => $player->hasCheckedIn(),
                    'throws' => array_map(
                        fn (DartThrow $throw): array => $this->serializeThrow($throw),
                        $throwHistoryByPlayer[$player->getId()] ?? [],
                    ),
                ],
                $players,
            ),
            'createdAt' => $game->getCreatedAt()->format(\DateTimeInterface::ATOM),
            'updatedAt' => $game->getUpdatedAt()->format(\DateTimeInterface::ATOM),
            'startedAt' => $game->getStartedAt()?->format(\DateTimeInterface::ATOM),
            'finishedAt' => $game->getFinishedAt()?->format(\DateTimeInterface::ATOM),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function serializeThrow(DartThrow $throw): array
    {
        return [
            'id' => $throw->getId(),
            'dartIndex' => $throw->getDartIndex(),
            'isBull' => $throw->isBull(),
            'isOut' => $throw->isOut(),
            'segment' => $throw->getSegment(),
            'multiplier' => $throw->getMultiplier(),
            'bullValue' => $throw->getBullValue(),
            'scoredPoints' => $throw->getScoredPoints(),
            'createdAt' => $throw->getCreatedAt()->format(\DateTimeInterface::ATOM),
        ];
    }
}
