<?php declare(strict_types=1);
/**
 * This file is part of the SplendidBear Websites' projects.
 *
 * Copyright (c) 2019 @ www.splendidbear.org
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Util;

use App\Entity\Grid;
use App\Entity\GridRow;
use App\Entity\PlayedGame;
use App\Entity\Step;
use App\Interfaces\RpcManagerInterface;
use App\Repository\StepRepository;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use JsonException;
use Psr\Log\LoggerInterface;
use Random\RandomException;
use RuntimeException;
use Symfony\Component\Uid\Uuid;

/**
 * Class RpcManager
 *
 * @package  App\Util
 * @author   Lang <https://www.splendidbear.org>
 * @category Class
 * @license  https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
 * @link     www.splendidbear.org
 * @since    2019. 10. 27.
 */
class RpcManager implements RpcManagerInterface
{
    private const int ROWS  = 16;
    private const int COLS  = 16;
    private const int MINES = 51;

    public function __construct(
        private readonly EntityManagerInterface $em,
        private readonly LoggerInterface        $logger,
        private readonly StepRepository         $stepRepository,
    ) {
    }

    public function getConnectInformation($params): string
    {
        $gameAssoc = is_array($params) ? $params[0] : $params;

        $playedGame = $this->em->getRepository(PlayedGame::class)->findOneByGameAssoc($gameAssoc);

        if (null === $playedGame) {
            try {
                return base64_encode(json_encode([
                    'users'           => null,
                    'revealedCells'   => null,
                    'lastStep'        => ['red' => null, 'blue' => null],
                    'mostRecentStep'  => null,
                    'redPoints'       => 0,
                    'bluePoints'      => 0,
                    'redBonusPoints'  => 0,
                    'blueBonusPoints' => 0,
                    'redBonusStats'   => [],
                    'blueBonusStats'  => [],
                    'gameFinished'    => false,
                ], JSON_THROW_ON_ERROR));
            } catch (JsonException $e) {
                throw new RuntimeException($e->getMessage());
            }
        }

        $users = $this->getUserCollection($playedGame);
        $revealedCells = $this->aggregateRevealedCells($playedGame);

        try {
            $redPoints = $playedGame->redPoints ?? 0;
            $bluePoints = $playedGame->bluePoints ?? 0;
            $gameFinished = $redPoints > 25 || $bluePoints > 25;

            return base64_encode(json_encode([
                'users'           => $users,
                'revealedCells'   => $revealedCells,
                'lastStep'        => $this->getLastStepPerPlayer($playedGame),
                'mostRecentStep'  => $this->getMostRecentStep($playedGame),
                'redPoints'       => $redPoints,
                'bluePoints'      => $bluePoints,
                'redBonusPoints'  => $playedGame->redBonusPoints ?? 0,
                'blueBonusPoints' => $playedGame->blueBonusPoints ?? 0,
                'redBonusStats'   => $playedGame->redBonusStats ?? [],
                'blueBonusStats'  => $playedGame->blueBonusStats ?? [],
                'gameFinished'    => $gameFinished,
            ], JSON_THROW_ON_ERROR));
        } catch (JsonException $e) {
            throw new RuntimeException($e->getMessage());
        }
    }

    /**
     * Get the most recent step of the game (if any).
     * Returns an array with player, row, col information or null if no steps exist.
     */
    private function getMostRecentStep(PlayedGame $playedGame): ?array
    {
        try {
            return $this->stepToArray($this->stepRepository->findMostRecent($playedGame));
        } catch (Exception $e) {
            $this->logger->error('Error getting most recent step: ' . $e->getMessage());
            return null;
        }
    }

    public function saveGrid(string $gameAssoc): bool
    {
        $existingGame = $this->em->getRepository(PlayedGame::class)->findOneByGameAssoc($gameAssoc);

        if (null !== $existingGame) {
            return true;
        }

        $grid2d = $this->generateGrid();
        $playedGame = new PlayedGame();
        $grid = new Grid();

        foreach ($grid2d as $row) {
            $gridRow = new GridRow();
            $gridRow->gridCol = $row;
            $gridRow->grid = $grid;
            $this->em->persist($gridRow);
        }

        $grid->playedGame = $playedGame;
        $this->em->persist($grid);

        $playedGame->gameAssoc = $gameAssoc;
        $playedGame->uuid = Uuid::fromString($gameAssoc);
        $playedGame->grid = $grid;
        $playedGame->created = new DateTime();
        $playedGame->updated = new DateTime();
        $this->em->persist($playedGame);

        $this->em->flush();

        return true;
    }

    /**
     * Generate a random 16×16 grid with 51 mines and adjacent-mine numbers.
     */
    private function generateGrid(): array
    {
        /** Build flat set: 51 mines ('m') + remaining water ('w') */
        $set = array_merge(
            array_fill(0, self::MINES, 'm'),
            array_fill(0, self::ROWS * self::COLS - self::MINES, 'w'),
        );

        /**
         * Fisher-Yates shuffle
         *
         * @see https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
         */
        for ($i = count($set) - 1; $i > 0; $i--) {
            try {
                $j = random_int(0, $i);
            } catch (RandomException $e) {
                throw new RuntimeException('Failed to generate random index: ' . $e->getMessage());
            }
            [$set[$i], $set[$j]] = [$set[$j], $set[$i]];
        }

        /** Reshape to 2-D */
        $grid = [];
        for ($r = 0; $r < self::ROWS; $r++) {
            $grid[$r] = array_slice($set, $r * self::COLS, self::COLS);
        }

        /** Replace 'w' with adjacent-mine count */
        $dirs = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];
        for ($r = 0; $r < self::ROWS; $r++) {
            for ($c = 0; $c < self::COLS; $c++) {
                if ('w' !== $grid[$r][$c]) {
                    continue;
                }
                $count = 0;
                foreach ($dirs as [$dr, $dc]) {
                    if (isset($grid[$r + $dr][$c + $dc]) && 'm' === $grid[$r + $dr][$c + $dc]) {
                        $count++;
                    }
                }
                $grid[$r][$c] = $count;
            }
        }

        return $grid;
    }

    /**
     * Collect all cells revealed so far, enriched with the player colour from each Step.
     */
    private function aggregateRevealedCells(PlayedGame $playedGame): array
    {
        $all = [];

        foreach ($playedGame->steps as $step) {
            if (null === $step->revealedCells) {
                continue;
            }
            $player = $step->player;
            foreach ($step->revealedCells as $cell) {
                $all[] = array_merge($cell, ['player' => $player]);
            }
        }

        return $all;
    }

    /**
     * Get the last step for each player.
     * Returns an array with 'red' and 'blue' keys, each containing row, col information or null if no steps exist for
     * that player.
     */
    private function getLastStepPerPlayer(PlayedGame $playedGame): array
    {
        try {
            return [
                'red'  => $this->stepToArray($this->stepRepository->findMostRecentForPlayer($playedGame, 'red')),
                'blue' => $this->stepToArray($this->stepRepository->findMostRecentForPlayer($playedGame, 'blue')),
            ];
        } catch (Exception $e) {
            $this->logger->error('Error getting last step per player: ' . $e->getMessage());
            return ['red' => null, 'blue' => null];
        }
    }

    private function stepToArray(?Step $step): ?array
    {
        if (null === $step) {
            return null;
        }

        return [
            'player' => $step->player,
            'row'    => (int)$step->row,
            'col'    => (int)$step->col,
        ];
    }

    private function getUserCollection(PlayedGame $playedGame): array
    {
        return [
            'red'      => null !== $playedGame->red ? $playedGame->red->getUsername() : '',
            'blue'     => null !== $playedGame->blue ? $playedGame->blue->getUsername() : '',
            'redAnon'  => null !== $playedGame->redAnon ? $playedGame->redAnon->userName : '',
            'blueAnon' => null !== $playedGame->blueAnon ? $playedGame->blueAnon->userName : '',
        ];
    }
}
