<?php

declare(strict_types=1);

namespace Tournament\BR;

use PDO;
use RuntimeException;
use Throwable;

final class BRRoomGenerator
{
    public function __construct(
        private readonly PDO $pdo
    ) {
    }

    /**
     * Generate Battle Royale rooms for a stage.
     *
     * @return array{
     *     stage_id:int,
     *     participant_count:int,
     *     room_size:int,
     *     rounds:int,
     *     rooms_per_round:int,
     *     total_rooms:int
     * }
     */
    public function generate(
        int $stageId
    ): array {
        $ownsTransaction =
            !$this->pdo->inTransaction();

        if ($ownsTransaction) {
            $this->pdo->beginTransaction();
        }

        try {
            $stage =
                $this->getStage($stageId);

            if ($stage['format'] !== 'battle_royale') {
                throw new RuntimeException(
                    'Stage format must be battle_royale.'
                );
            }

            $roomSize =
                (int) $stage['match_size'];

            $matchesPerPlayer =
                (int) $stage['matches_per_player'];

            if ($roomSize < 2) {
                throw new RuntimeException(
                    'Invalid Battle Royale room size.'
                );
            }

            if ($matchesPerPlayer < 1) {
                throw new RuntimeException(
                    'Invalid matches per player.'
                );
            }

            $participants =
                $this->getParticipants($stageId);

            $participantCount =
                count($participants);

            if ($participantCount < 2) {
                throw new RuntimeException(
                    'At least 2 participants are required.'
                );
            }

            /*
             * جلوگیری از Generate دوباره.
             */
            $this->assertNotGenerated(
                $stageId
            );

            /*
             * برای هر Round بازیکن‌ها Shuffle می‌شوند.
             */
            $allRooms = 0;

            for (
                $round = 1;
                $round <= $matchesPerPlayer;
                $round++
            ) {
                $roundParticipants =
                    $participants;

                shuffle($roundParticipants);

                $rooms =
                    array_chunk(
                        $roundParticipants,
                        $roomSize
                    );

                foreach (
                    $rooms as $roomIndex => $roomParticipants
                ) {
                    $roomNumber =
                        $roomIndex + 1;

                    $roomId =
                        $this->createRoom(
                            stageId: $stageId,
                            roundNumber: $round,
                            roomNumber: $roomNumber,
                            capacity: $roomSize,
                            participantCount:
                                count($roomParticipants)
                        );

                    $slot = 1;

                    foreach (
                        $roomParticipants
                        as $participant
                    ) {
                        $this->addParticipant(
                            roomId: $roomId,
                            participantId:
                                (int) $participant['id'],
                            slot: $slot
                        );

                        $slot++;
                    }

                    $this->markRoomReady(
                        $roomId
                    );

                    $allRooms++;
                }
            }

            if ($ownsTransaction) {
                $this->pdo->commit();
            }

            return [
                'stage_id' =>
                    $stageId,

                'participant_count' =>
                    $participantCount,

                'room_size' =>
                    $roomSize,

                'rounds' =>
                    $matchesPerPlayer,

                'rooms_per_round' =>
                    (int) ceil(
                        $participantCount /
                        $roomSize
                    ),

                'total_rooms' =>
                    $allRooms,
            ];

        } catch (Throwable $e) {

            if (
                $ownsTransaction
                && $this->pdo->inTransaction()
            ) {
                $this->pdo->rollBack();
            }

            throw $e;
        }
    }

    private function getStage(
        int $stageId
    ): array {
        $statement =
            $this->pdo->prepare("
                SELECT
                    id,
                    ev_id,
                    name,
                    type,
                    format,
                    match_size,
                    matches_per_player,
                    advancement_type,
                    advancement_count,
                    scoring_strategy,
                    status
                FROM tournament_stages
                WHERE id = ?
                LIMIT 1
            ");

        $statement->execute([
            $stageId
        ]);

        $stage =
            $statement->fetch(
                PDO::FETCH_ASSOC
            );

        if (!$stage) {
            throw new RuntimeException(
                'Stage not found.'
            );
        }

        return $stage;
    }

    private function getParticipants(
        int $stageId
    ): array {
        $statement =
            $this->pdo->prepare("
                SELECT
                    tp.id,
                    tp.event_player_id,
                    tp.seed,
                    ep.ign,
                    ep.user_id
                FROM tournament_participants tp
                INNER JOIN event_players ep
                    ON ep.id = tp.event_player_id
                INNER JOIN tournament_stages ts
                    ON ts.ev_id = ep.ev_id
                WHERE ts.id = ?
                  AND tp.status = 'active'
                  AND ep.status = 'accepted'
                ORDER BY tp.id ASC
            ");

        $statement->execute([
            $stageId
        ]);

        return $statement->fetchAll(
            PDO::FETCH_ASSOC
        );
    }

    private function assertNotGenerated(
        int $stageId
    ): void {
        $statement =
            $this->pdo->prepare("
                SELECT COUNT(*)
                FROM tournament_br_rooms
                WHERE stage_id = ?
            ");

        $statement->execute([
            $stageId
        ]);

        if (
            (int) $statement->fetchColumn() > 0
        ) {
            throw new RuntimeException(
                'Battle Royale rooms have already been generated.'
            );
        }
    }

    private function createRoom(
        int $stageId,
        int $roundNumber,
        int $roomNumber,
        int $capacity,
        int $participantCount
    ): int {
        $statement =
            $this->pdo->prepare("
                INSERT INTO tournament_br_rooms (
                    stage_id,
                    round_number,
                    room_number,
                    capacity,
                    participant_count,
                    status
                )
                VALUES (?, ?, ?, ?, ?, 'pending')
            ");

        $statement->execute([
            $stageId,
            $roundNumber,
            $roomNumber,
            $capacity,
            $participantCount
        ]);

        return (int)
            $this->pdo->lastInsertId();
    }

    private function addParticipant(
        int $roomId,
        int $participantId,
        int $slot
    ): void {
        $statement =
            $this->pdo->prepare("
                INSERT INTO tournament_br_room_participants (
                    room_id,
                    tournament_participant_id,
                    slot_number,
                    status
                )
                VALUES (?, ?, ?, 'ready')
            ");

        $statement->execute([
            $roomId,
            $participantId,
            $slot
        ]);
    }

    private function markRoomReady(
        int $roomId
    ): void {
        $statement =
            $this->pdo->prepare("
                UPDATE tournament_br_rooms
                SET status = 'ready'
                WHERE id = ?
            ");

        $statement->execute([
            $roomId
        ]);
    }
}