<?php

declare(strict_types=1);

namespace App\Service;

use App\Entity\DartThrow;
use App\Entity\Game;
use App\Entity\GameEventLog;
use App\Entity\GamePlayer;
use App\Entity\Turn;
use App\Enum\GameEventType;
use App\Enum\GameModeType;
use App\Enum\GameStatus;
use App\Enum\ThrowRule;
use App\Repository\DartThrowRepository;
use App\Repository\TurnRepository;
use Doctrine\ORM\EntityManagerInterface;

class GameEngine
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly TurnRepository $turnRepository,
        private readonly DartThrowRepository $throwRepository,
    ) {
    }

    public function startGame(Game $game): void
    {
        $players = $this->getOrderedPlayers($game);
        if ($players === []) {
            throw new \RuntimeException('At least one player is required to start a game.');
        }

        $startScore = $game->getMode()->getStartScore();
        foreach ($players as $player) {
            $player->setCurrentScore($startScore);
            $player->setIsWinner(false);
            $player->setHasCheckedIn(
                $game->getMode()->getType() === GameModeType::ATW
                    || $game->getInRule() === ThrowRule::ANY,
            );
        }

        $game->setStatus(GameStatus::RUNNING)
            ->setStartedAt(new \DateTimeImmutable())
            ->setFinishedAt(null)
            ->setCurrentPlayerIndex(0)
            ->setCurrentTurnNumber(1)
            ->setCurrentDartInTurn(1);

        $this->logEvent($game, GameEventType::GAME_STARTED, [
            'startScore' => $startScore,
            'players' => array_map(static fn (GamePlayer $player): string => $player->getName(), $players),
        ]);

        $this->entityManager->flush();
    }

    public function commitThrow(Game $game, int|string $segmentOrBull, int $multiplierOrBullValue): DartThrow
    {
        if ($game->getStatus() !== GameStatus::RUNNING) {
            throw new \RuntimeException('Game is not running.');
        }

        $isBull = false;
        $segment = null;
        $multiplier = null;
        $bullValue = null;

        if (\is_string($segmentOrBull)) {
            if ($segmentOrBull === 'BULL') {
                if (!\in_array($multiplierOrBullValue, [25, 50], true)) {
                    throw new \InvalidArgumentException('Bull value must be 25 or 50.');
                }

                $isBull = true;
                $bullValue = $multiplierOrBullValue;
                $scoredPoints = $bullValue;
            } elseif ($segmentOrBull === 'OUT') {
                $scoredPoints = 0;
            } else {
                throw new \InvalidArgumentException('Unsupported segment string.');
            }
        } else {
            if ($segmentOrBull < 1 || $segmentOrBull > 20) {
                throw new \InvalidArgumentException('Segment must be between 1 and 20.');
            }

            if (!\in_array($multiplierOrBullValue, [1, 2, 3], true)) {
                throw new \InvalidArgumentException('Multiplier must be 1, 2 or 3.');
            }

            $segment = $segmentOrBull;
            $multiplier = $multiplierOrBullValue;
            $scoredPoints = $segment * $multiplier;
        }

        $throw = $this->commitThrowInternal(
            game: $game,
            isBull: $isBull,
            scoredPoints: $scoredPoints,
            segment: $segment,
            multiplier: $multiplier,
            bullValue: $bullValue,
            flush: true,
            recordEvent: true,
        );

        return $throw;
    }

    public function undoLastThrow(Game $game): void
    {
        $existingThrows = $this->throwRepository->findAllForGameOrdered($game);
        if ($existingThrows === []) {
            return;
        }

        $throwHistory = [];
        foreach ($existingThrows as $throw) {
            $throwHistory[] = [
                'isBull' => $throw->isBull(),
                'segment' => $throw->getSegment(),
                'multiplier' => $throw->getMultiplier(),
                'bullValue' => $throw->getBullValue(),
                'scoredPoints' => $throw->getScoredPoints(),
                'createdAt' => $throw->getCreatedAt(),
            ];
        }

        $removedThrow = array_pop($throwHistory);
        if ($removedThrow === null) {
            return;
        }

        foreach (iterator_to_array($game->getTurns()) as $turn) {
            $game->removeTurn($turn);
            $turn->getPlayer()->removeTurn($turn);
            $this->entityManager->remove($turn);
        }

        foreach (iterator_to_array($game->getEventLogs()) as $eventLog) {
            $this->entityManager->remove($eventLog);
        }
        $game->getEventLogs()->clear();

        $this->entityManager->flush();

        $this->resetStateForReplay($game);

        $this->logEvent($game, GameEventType::GAME_STARTED, [
            'replayed' => true,
            'startScore' => $game->getMode()->getStartScore(),
        ]);

        foreach ($throwHistory as $throwData) {
            $this->commitThrowInternal(
                game: $game,
                isBull: $throwData['isBull'],
                scoredPoints: $throwData['scoredPoints'],
                segment: $throwData['segment'],
                multiplier: $throwData['multiplier'],
                bullValue: $throwData['bullValue'],
                flush: false,
                recordEvent: true,
                createdAt: $throwData['createdAt'],
            );
        }

        $this->recomputeCursorFromTurns($game);

        $this->logEvent($game, GameEventType::THROW_UNDONE, [
            'segment' => $removedThrow['segment'],
            'multiplier' => $removedThrow['multiplier'],
            'bullValue' => $removedThrow['bullValue'],
            'scoredPoints' => $removedThrow['scoredPoints'],
        ]);

        $this->entityManager->flush();
    }

    public function getActivePlayer(Game $game): GamePlayer
    {
        $players = $this->getOrderedPlayers($game);
        if ($players === []) {
            throw new \RuntimeException('Game has no players.');
        }

        $index = $game->getCurrentPlayerIndex();
        if ($index < 0 || $index >= \count($players)) {
            $index = 0;
            $game->setCurrentPlayerIndex(0);
        }

        return $players[$index];
    }

    private function commitThrowInternal(
        Game $game,
        bool $isBull,
        int $scoredPoints,
        ?int $segment,
        ?int $multiplier,
        ?int $bullValue,
        bool $flush,
        bool $recordEvent,
        ?\DateTimeImmutable $createdAt = null,
    ): DartThrow {
        $activePlayer = $this->getActivePlayer($game);
        $turn = $this->findOpenTurn($game);

        if ($turn === null) {
            $turn = new Turn(
                game: $game,
                player: $activePlayer,
                turnNumber: $game->getCurrentTurnNumber(),
                playerTurnIndex: $this->getNextPlayerTurnIndex($activePlayer),
                startScore: $activePlayer->getCurrentScore(),
            );
            $game->addTurn($turn);
            $activePlayer->addTurn($turn);
            $this->entityManager->persist($turn);
        }

        if ($turn->getPlayer()->getId() !== $activePlayer->getId()) {
            throw new \LogicException('Open turn does not belong to active player.');
        }

        $dartIndex = $game->getCurrentDartInTurn();

        $throw = new DartThrow(
            game: $game,
            turn: $turn,
            player: $activePlayer,
            dartIndex: $dartIndex,
            scoredPoints: $scoredPoints,
            isBull: $isBull,
            segment: $segment,
            multiplier: $multiplier,
            bullValue: $bullValue,
            createdAt: $createdAt,
        );

        $turn->addThrow($throw);
        $this->entityManager->persist($throw);

        $eventContext = ['modeType' => $game->getMode()->getType()->value];

        if ($game->getMode()->getType() === GameModeType::ATW) {
            $eventContext = array_merge(
                $eventContext,
                $this->resolveAroundTheWorldThrow(
                    game: $game,
                    turn: $turn,
                    activePlayer: $activePlayer,
                    dartIndex: $dartIndex,
                    segment: $segment,
                    isBull: $isBull,
                    recordEvent: $recordEvent,
                ),
            );
        } else {
            $canScoreThisThrow = $activePlayer->hasCheckedIn();
            if (!$canScoreThisThrow && $this->throwMatchesRule($game->getInRule(), $isBull, $multiplier, $bullValue)) {
                $activePlayer->setHasCheckedIn(true);
                $canScoreThisThrow = true;
            }

            if (!$canScoreThisThrow) {
                if ($dartIndex >= 3) {
                    $this->finalizeTurn($game, $turn, $activePlayer->getCurrentScore(), false, $recordEvent);
                    $this->advanceToNextPlayer($game);
                } else {
                    $game->setCurrentDartInTurn($dartIndex + 1);
                }
            } else {
                $hypotheticalScore = $activePlayer->getCurrentScore() - $scoredPoints;

                if ($hypotheticalScore < 0) {
                    $activePlayer->setCurrentScore($turn->getStartScore());
                    $this->finalizeTurn($game, $turn, $turn->getStartScore(), true, $recordEvent);
                    $this->advanceToNextPlayer($game);
                } elseif ($hypotheticalScore === 0) {
                    if ($this->throwMatchesRule($game->getOutRule(), $isBull, $multiplier, $bullValue)) {
                        $activePlayer->setCurrentScore(0);
                        $activePlayer->setIsWinner(true);
                        $this->finalizeTurn($game, $turn, 0, false, $recordEvent);
                        $game->setStatus(GameStatus::FINISHED)
                            ->setFinishedAt(new \DateTimeImmutable());

                        if ($recordEvent) {
                            $this->logEvent($game, GameEventType::GAME_FINISHED, [
                                'winner' => $activePlayer->getName(),
                            ]);
                        }
                    } else {
                        $activePlayer->setCurrentScore($turn->getStartScore());
                        $this->finalizeTurn($game, $turn, $turn->getStartScore(), true, $recordEvent);
                        $this->advanceToNextPlayer($game);
                    }
                } elseif ($this->isDeadScoreForOutRule($hypotheticalScore, $game->getOutRule())) {
                    $activePlayer->setCurrentScore($turn->getStartScore());
                    $this->finalizeTurn($game, $turn, $turn->getStartScore(), true, $recordEvent);
                    $this->advanceToNextPlayer($game);
                } else {
                    $activePlayer->setCurrentScore($hypotheticalScore);
                    if ($dartIndex >= 3) {
                        $this->finalizeTurn($game, $turn, $hypotheticalScore, false, $recordEvent);
                        $this->advanceToNextPlayer($game);
                    } else {
                        $game->setCurrentDartInTurn($dartIndex + 1);
                    }
                }
            }
        }

        if ($recordEvent) {
            $this->logEvent($game, GameEventType::THROW_COMMITTED, [
                'player' => $activePlayer->getName(),
                'dartIndex' => $dartIndex,
                'segment' => $segment,
                'multiplier' => $multiplier,
                'bullValue' => $bullValue,
                'scoredPoints' => $scoredPoints,
                'scoreAfter' => $activePlayer->getCurrentScore(),
                'checkedIn' => $activePlayer->hasCheckedIn(),
                ...$eventContext,
            ]);
        }

        if ($flush) {
            $this->entityManager->flush();
        }

        return $throw;
    }

    private function findOpenTurn(Game $game): ?Turn
    {
        $openTurns = array_values(array_filter(
            $game->getTurns()->toArray(),
            static fn (Turn $turn): bool => $turn->getEndedAt() === null,
        ));

        if ($openTurns !== []) {
            usort(
                $openTurns,
                static fn (Turn $a, Turn $b): int => $b->getTurnNumber() <=> $a->getTurnNumber(),
            );

            return $openTurns[0];
        }

        return $this->turnRepository->findOpenTurn($game);
    }

    private function finalizeTurn(Game $game, Turn $turn, int $endScore, bool $isBust, bool $recordEvent): void
    {
        $turn
            ->setEndScore($endScore)
            ->setIsBust($isBust)
            ->setTotalScored($isBust ? 0 : max(0, $turn->getStartScore() - $endScore))
            ->setEndedAt(new \DateTimeImmutable());

        if ($recordEvent) {
            $this->logEvent($game, GameEventType::TURN_ENDED, [
                'turnNumber' => $turn->getTurnNumber(),
                'player' => $turn->getPlayer()->getName(),
                'isBust' => $isBust,
                'startScore' => $turn->getStartScore(),
                'endScore' => $endScore,
            ]);
        }
    }

    private function advanceToNextPlayer(Game $game): void
    {
        if ($game->getStatus() === GameStatus::FINISHED) {
            return;
        }

        $players = $this->getOrderedPlayers($game);
        $countPlayers = \count($players);
        if ($countPlayers === 0) {
            return;
        }

        $nextPlayerIndex = ($game->getCurrentPlayerIndex() + 1) % $countPlayers;

        $game->setCurrentPlayerIndex($nextPlayerIndex)
            ->setCurrentDartInTurn(1)
            ->setCurrentTurnNumber($game->getCurrentTurnNumber() + 1);
    }

    private function getNextPlayerTurnIndex(GamePlayer $player): int
    {
        $currentMax = null;
        foreach ($player->getTurns() as $turn) {
            $index = $turn->getPlayerTurnIndex();
            if ($index === null) {
                continue;
            }

            $currentMax = $currentMax === null ? $index : max($currentMax, $index);
        }

        if ($currentMax !== null) {
            return $currentMax + 1;
        }

        $last = $this->turnRepository->findLastPlayerTurnIndex($player);

        return ($last ?? 0) + 1;
    }

    /** @return list<GamePlayer> */
    private function getOrderedPlayers(Game $game): array
    {
        $players = $game->getPlayers()->toArray();
        usort($players, static fn (GamePlayer $a, GamePlayer $b): int => $a->getPosition() <=> $b->getPosition());

        return $players;
    }

    private function logEvent(Game $game, GameEventType $type, array $payload): void
    {
        $event = new GameEventLog($game, $type, $payload);
        $game->addEventLog($event);
        $this->entityManager->persist($event);
    }

    private function resetStateForReplay(Game $game): void
    {
        $startScore = $game->getMode()->getStartScore();
        foreach ($this->getOrderedPlayers($game) as $player) {
            $player->setCurrentScore($startScore);
            $player->setIsWinner(false);
            $player->setHasCheckedIn(
                $game->getMode()->getType() === GameModeType::ATW
                    || $game->getInRule() === ThrowRule::ANY,
            );
        }

        $game->setStatus(GameStatus::RUNNING)
            ->setFinishedAt(null)
            ->setCurrentPlayerIndex(0)
            ->setCurrentTurnNumber(1)
            ->setCurrentDartInTurn(1);
    }

    private function recomputeCursorFromTurns(Game $game): void
    {
        $players = $this->getOrderedPlayers($game);
        if ($players === []) {
            return;
        }

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

        $turns = $game->getTurns()->toArray();
        usort($turns, static fn (Turn $a, Turn $b): int => $a->getTurnNumber() <=> $b->getTurnNumber());

        if ($turns === []) {
            $game->setCurrentPlayerIndex(0)
                ->setCurrentTurnNumber(1)
                ->setCurrentDartInTurn(1);

            return;
        }

        $openTurns = array_values(array_filter(
            $turns,
            static fn (Turn $turn): bool => $turn->getEndedAt() === null,
        ));

        if ($openTurns !== []) {
            usort($openTurns, static fn (Turn $a, Turn $b): int => $b->getTurnNumber() <=> $a->getTurnNumber());
            $openTurn = $openTurns[0];
            $throwsInTurn = $openTurn->getThrows()->count();
            $nextDart = min(3, max(1, $throwsInTurn + 1));

            $game->setCurrentPlayerIndex($playerIndexById[$openTurn->getPlayer()->getId()] ?? 0)
                ->setCurrentTurnNumber($openTurn->getTurnNumber())
                ->setCurrentDartInTurn($nextDart);

            return;
        }

        $lastTurn = $turns[array_key_last($turns)];
        $lastPlayerIndex = $playerIndexById[$lastTurn->getPlayer()->getId()] ?? 0;
        $nextPlayerIndex = ($lastPlayerIndex + 1) % count($players);

        $game->setCurrentPlayerIndex($nextPlayerIndex)
            ->setCurrentTurnNumber($lastTurn->getTurnNumber() + 1)
            ->setCurrentDartInTurn(1);
    }

    private function throwMatchesRule(ThrowRule $rule, bool $isBull, ?int $multiplier, ?int $bullValue): bool
    {
        if ($rule === ThrowRule::ANY) {
            return true;
        }

        if ($isBull) {
            return match ($rule) {
                ThrowRule::SINGLE => $bullValue === 25,
                ThrowRule::DOUBLE => $bullValue === 50,
                ThrowRule::TRIPLE => false,
                ThrowRule::ANY => true,
            };
        }

        return match ($rule) {
            ThrowRule::SINGLE => $multiplier === 1,
            ThrowRule::DOUBLE => $multiplier === 2,
            ThrowRule::TRIPLE => $multiplier === 3,
            ThrowRule::ANY => true,
        };
    }

    private function isDeadScoreForOutRule(int $remainingScore, ThrowRule $outRule): bool
    {
        if ($remainingScore <= 0) {
            return false;
        }

        return $remainingScore < match ($outRule) {
            ThrowRule::ANY, ThrowRule::SINGLE => 1,
            ThrowRule::DOUBLE => 2,
            ThrowRule::TRIPLE => 3,
        };
    }

    /**
     * @return array{targetBefore:int,targetAfter:int,hitTarget:bool}
     */
    private function resolveAroundTheWorldThrow(
        Game $game,
        Turn $turn,
        GamePlayer $activePlayer,
        int $dartIndex,
        ?int $segment,
        bool $isBull,
        bool $recordEvent,
    ): array {
        $rawTarget = $activePlayer->getCurrentScore();
        $targetBefore = $rawTarget >= 21 ? 21 : max(1, min(20, $rawTarget));
        $isBullStage = $targetBefore === 21;
        $hitTarget = $isBullStage
            ? $isBull
            : (!$isBull && $segment === $targetBefore);

        if ($hitTarget) {
            if ($isBullStage) {
                $activePlayer->setCurrentScore(21);
                $activePlayer->setIsWinner(true);
                $this->finalizeTurn($game, $turn, 21, false, $recordEvent);
                $game->setStatus(GameStatus::FINISHED)
                    ->setFinishedAt(new \DateTimeImmutable());

                if ($recordEvent) {
                    $this->logEvent($game, GameEventType::GAME_FINISHED, [
                        'winner' => $activePlayer->getName(),
                    ]);
                }
            } elseif ($targetBefore === 20) {
                $activePlayer->setCurrentScore(21);
            } else {
                $activePlayer->setCurrentScore($targetBefore + 1);
            }
        }

        if ($game->getStatus() !== GameStatus::FINISHED) {
            if ($dartIndex >= 3) {
                $this->finalizeTurn($game, $turn, $activePlayer->getCurrentScore(), false, $recordEvent);
                $this->advanceToNextPlayer($game);
            } else {
                $game->setCurrentDartInTurn($dartIndex + 1);
            }
        }

        return [
            'targetBefore' => $targetBefore,
            'targetAfter' => $activePlayer->getCurrentScore(),
            'hitTarget' => $hitTarget,
        ];
    }
}
