<?php
declare(strict_types=1);

namespace App\Game\Service;

use App\Game\Entity\Game;
use App\Game\Entity\Player;
use App\Game\Entity\Session;
use App\Game\Entity\SessionSetting;
use App\Game\Enum\GameStatus;
use App\Game\Enum\SessionSettingType;
use App\Game\Enum\SessionStatus;
use App\Game\Repository\GameRepository;
use App\Game\Repository\SessionRepository;
use App\Tech\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;

final class GameDashboardService
{
    public function __construct(
        private readonly GameRepository $gameRepository,
        private readonly SessionRepository $sessionRepository,
        private readonly EntityManagerInterface $entityManager,
        private readonly HubInterface $hub,
    ) {
    }

    /**
     * @return Session[]
     */
    public function getSessionsForUser(UserInterface $user): array
    {
        return $this->sessionRepository->createQueryBuilder('s')
            ->innerJoin('s.players', 'p')
            ->where('p.user = :user')
            ->setParameter('user', $user)
            ->getQuery()
            ->getResult();
    }

    /**
     * @return Game[]
     */
    public function getAvailableGames(bool $isAdmin): array
    {
        if ($isAdmin) {
            return $this->gameRepository->findAll();
        }

        return $this->gameRepository->findBy(['status' => GameStatus::OPEN]);
    }

    public function createSession(Game $game, UserInterface $user, bool $isAdmin): ?Session
    {
        if ($game->getStatus() !== GameStatus::OPEN && !$isAdmin) {
            return null;
        }

        if(!$user instanceof User)
            return null;

        $session = new Session();
        $session->setGame($game);
        $session->setStatus(SessionStatus::CREATED);
        $session->setTimer(0);

        $player = new Player();
        $player->setUser($user);
        $session->addPlayer($player);

        $this->entityManager->persist($session);
        $this->entityManager->persist($player);

        $this->entityManager->flush();

        if (count($session->getPlayers()) === $session->getGame()->getNumberOfPlayers()) {
            $this->startSession($session);
        }

        return $session;
    }

    public function joinSession(string $inviteCode, UserInterface $user): bool
    {
        if (!$user instanceof User) {
            return false;
        }

        $setting = $this->entityManager->getRepository(SessionSetting::class)->findOneBy([
            'name' => SessionSettingType::INVITE_CODE,
            'value' => $inviteCode,
        ]);

        if (!$setting) {
            return false;
        }

        $session = $setting->getSession();

        // Check if user is already in this session
        foreach ($session->getPlayers() as $player) {
            if ($player->getUser() === $user) {
                return true; // Already joined
            }
        }

        $playerCount = count($session->getPlayers());
        if ($playerCount >= $session->getGame()->getNumberOfPlayers()) {
            return false; // Session full
        }

        $player = new Player();
        $player->setUser($user);
        $session->addPlayer($player);

        $this->entityManager->persist($player);

        $this->entityManager->flush();

        if (count($session->getPlayers()) === $session->getGame()->getNumberOfPlayers()) {
            $this->startSession($session);
        }

        return true;
    }

    public function leaveSession(Session $session, UserInterface $user): bool
    {
        if (!$user instanceof User) {
            return false;
        }

        if (!in_array($session->getStatus(), [SessionStatus::CREATED, SessionStatus::READY]) || $session->getTimer() > 0) {
            return false;
        }

        $playerToDelete = null;
        foreach ($session->getPlayers() as $player) {
            if ($player->getUser() === $user) {
                $playerToDelete = $player;
                break;
            }
        }

        if (!$playerToDelete) {
            return false;
        }

        if ($session->getStatus() === SessionStatus::READY) {
            $session->setStatus(SessionStatus::CREATED);
            // Clear assignments for all remaining players since the game needs to be "started" again
            foreach ($session->getPlayers() as $player) {
                $player->setScreen(null);
            }
        }

        // Remove player specific settings (like rights)
        foreach ($session->getSettings() as $setting) {
            if ($setting->getPlayer() === $playerToDelete) {
                $session->removeSetting($setting);
                $this->entityManager->remove($setting);
            }
        }

        $session->removePlayer($playerToDelete);
        $this->entityManager->remove($playerToDelete);

        // If no players left, we might want to delete the session and its remaining settings
        if ($session->getPlayers()->isEmpty()) {
            foreach ($session->getSettings() as $setting) {
                $this->entityManager->remove($setting);
            }
            $this->entityManager->remove($session);
        }

        $this->entityManager->flush();

        return true;
    }

    private function initializePlayerSettings(Player $player): void
    {
        $screen = $player->getScreen();
        $rightsSettingName = SessionSettingType::tryFrom('RightsForPlayer' . $screen);

        if ($rightsSettingName) {
            $setting = new SessionSetting();
            $setting->setSession($player->getSession());
            $setting->setPlayer($player);
            $setting->setName($rightsSettingName);
            $setting->setValue(json_encode(['chat', 'help', 'ls', 'pwd']));
            $this->entityManager->persist($setting);
        }

        $pwdSettingName = SessionSettingType::tryFrom('PwdForPlayer' . $screen);

        if ($pwdSettingName) {
            $setting = new SessionSetting();
            $setting->setSession($player->getSession());
            $setting->setPlayer($player);
            $setting->setName($pwdSettingName);
            $setting->setValue('/var/home/' . $player->getUser()->getUsername());
            $this->entityManager->persist($setting);
        }

        $chatTrackingSettingName = SessionSettingType::tryFrom('ChatTrackingForPlayer' . $screen);

        if ($chatTrackingSettingName) {
            $setting = new SessionSetting();
            $setting->setSession($player->getSession());
            $setting->setPlayer($player);
            $setting->setName($chatTrackingSettingName);
            $setting->setValue(json_encode([]));
            $this->entityManager->persist($setting);
        }

        $verifyCodesSettingName = SessionSettingType::tryFrom('VerifyCodesForPlayer' . $screen);

        if ($verifyCodesSettingName) {
            $codes = [];
            $numPlayers = $player->getSession()->getGame()->getNumberOfPlayers();
            for ($i = 1; $i <= $numPlayers; $i++) {
                if ($i !== $screen) {
                    $codes[$i] = bin2hex(random_bytes(3)); // 6 characters code
                }
            }

            $setting = new SessionSetting();
            $setting->setSession($player->getSession());
            $setting->setPlayer($player);
            $setting->setName($verifyCodesSettingName);
            $setting->setValue(json_encode($codes));
            $this->entityManager->persist($setting);
        }

        $verificationProgressSettingName = SessionSettingType::tryFrom('VerificationProgressForPlayer' . $screen);

        if ($verificationProgressSettingName) {
            $setting = new SessionSetting();
            $setting->setSession($player->getSession());
            $setting->setPlayer($player);
            $setting->setName($verificationProgressSettingName);
            $setting->setValue(json_encode([]));
            $this->entityManager->persist($setting);
        }
    }

    public function startSession(Session $session): bool
    {
        if ($session->getStatus() !== SessionStatus::CREATED) {
            return false;
        }

        $players = $session->getPlayers()->toArray();
        if (count($players) < $session->getGame()->getNumberOfPlayers()) {
            return false;
        }

        // Clean up any existing assignments (e.g. if we reverted from READY to CREATED)
        foreach ($session->getSettings() as $setting) {
            if ($setting->getPlayer() !== null) {
                $session->removeSetting($setting);
                $this->entityManager->remove($setting);
            }
        }

        // Shuffle players to assign random screens
        shuffle($players);

        foreach ($players as $index => $player) {
            $screen = $index + 1;
            $player->setScreen($screen);
            $this->initializePlayerSettings($player);
            $this->entityManager->persist($player);
        }

        $session->setStatus(SessionStatus::READY);
        $this->entityManager->persist($session);
        $this->entityManager->flush();

        return true;
    }

    public function toggleReady(Session $session, User $user): bool
    {
        if ($session->getStatus() !== SessionStatus::READY) {
            return false;
        }

        $player = null;
        foreach ($session->getPlayers() as $p) {
            if ($p->getUser() === $user) {
                $player = $p;
                break;
            }
        }

        if (!$player) {
            return false;
        }

        $settingName = SessionSettingType::tryFrom('ReadyAtForPlayer' . $player->getScreen());
        if (!$settingName) {
            return false;
        }

        /** @var \App\Game\Repository\SessionSettingRepository $settingRepo */
        $settingRepo = $this->entityManager->getRepository(SessionSetting::class);
        $setting = $settingRepo->getSetting($session, $settingName, $player);

        if ($setting) {
            $session->removeSetting($setting);
            $this->entityManager->remove($setting);
        } else {
            $setting = new SessionSetting();
            $setting->setSession($session);
            $setting->setPlayer($player);
            $setting->setName($settingName);
            $setting->setValue((string)(new \DateTime())->getTimestamp());
            $this->entityManager->persist($setting);
        }

        $this->checkAllPlayersReady($session);
        $this->entityManager->flush();

        // If this toggle just made everyone ready, checkAllPlayersReady() already
        // transitioned the session out of READY and published 'all_ready' —
        // don't also publish a redundant 'player_ready'.
        if ($session->getStatus() === SessionStatus::READY) {
            try {
                $topic = '/game/hub/' . $session->getId();
                $this->hub->publish(new Update($topic, json_encode(['type' => 'player_ready', 'player' => $player->getScreen(), 'ready' => !$setting])));
            } catch (\Exception $e) {
                // Mercure might be down, but we don't want to crash the game
            }
        }

        return true;
    }

    public function checkAllPlayersReady(Session $session): void
    {
        if ($session->getStatus() !== SessionStatus::READY) {
            return;
        }

        $players = $session->getPlayers();
        $numPlayers = $session->getGame()->getNumberOfPlayers();

        if (count($players) < $numPlayers) {
            return;
        }

        $readyPlayersCount = 0;

        /** @var \App\Game\Repository\SessionSettingRepository $settingRepo */
        $settingRepo = $this->entityManager->getRepository(SessionSetting::class);

        foreach ($players as $player) {
            $settingName = SessionSettingType::tryFrom('ReadyAtForPlayer' . $player->getScreen());
            if (!$settingName) {
                continue;
            }

            if ($settingRepo->getSetting($session, $settingName, $player)) {
                $readyPlayersCount++;
            }
        }

        if ($readyPlayersCount === $numPlayers) {
            $session->setStatus(SessionStatus::PLAYING);

            // Set the end timer
            $game = $session->getGame();
            /** @var \App\Game\Repository\GameSettingRepository $gameSettingRepo */
            $gameSettingRepo = $this->entityManager->getRepository(\App\Game\Entity\GameSetting::class);
            $totalTimeSetting = $gameSettingRepo->getSetting($game, \App\Game\Enum\GameSettingType::TOTAL_TIME);
            $totalTime = $totalTimeSetting ? (int)$totalTimeSetting->getValue() : 3600; // Default to 60 minutes if not set

            $session->setTimer((new \DateTime())->getTimestamp() + $totalTime);

            $this->entityManager->persist($session);

            // Clean up ready settings
            foreach ($players as $player) {
                $settingName = SessionSettingType::tryFrom('ReadyAtForPlayer' . $player->getScreen());
                if ($settingName) {
                    $setting = $settingRepo->getSetting($session, $settingName, $player);
                    if ($setting) {
                        $session->removeSetting($setting);
                        $this->entityManager->remove($setting);
                    }
                }
            }

            try {
                $topic = '/game/hub/' . $session->getId();
                $this->hub->publish(new Update($topic, json_encode(['type' => 'all_ready'])));
            } catch (\Exception $e) {
                // Mercure might be down, but we don't want to crash the game
            }
        }
    }

    public function generateInviteCode(Session $session, UserInterface $user, bool $isAdmin): ?string
    {
        // Security check: is user part of this session?
        $isPlayer = false;
        foreach ($session->getPlayers() as $player) {
            if ($player->getUser() === $user) {
                $isPlayer = true;
                break;
            }
        }

        if (!$isPlayer && !$isAdmin) {
            return null;
        }

        $inviteCode = bin2hex(random_bytes(4));

        $setting = null;
        foreach ($session->getSettings() as $s) {
            if ($s->getName() === SessionSettingType::INVITE_CODE) {
                $setting = $s;
                break;
            }
        }

        if (!$setting) {
            $setting = new SessionSetting();
            $setting->setSession($session);
            $setting->setName(SessionSettingType::INVITE_CODE);
        }

        $setting->setValue($inviteCode);
        $this->entityManager->persist($setting);
        $this->entityManager->flush();

        return $inviteCode;
    }
}
