<?php

declare(strict_types=1);

namespace Tournament\Stage;

use PDO;
use RuntimeException;
use Throwable;

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

    /**
     * Generate Battle Royale rooms for a stage.
     *
     * قانون اصلی:
     *
     * Qualification / Group:
     * - Groupها یک بار به صورت Random ساخته می‌شوند.
     *
     * Playoff / Final:
     * - Groupها یک بار بر اساس Seed ساخته می‌شوند.
     *
     * در هر دو حالت:
     * - Group Assignment ثابت است.
     * - تمام Roundهای یک Group با همان بازیکنان انجام می‌شود.
     *
     * مثال:
     *
     * Group 1 = Player A,B,C,...
     *
     * Round 1 → Group 1
     * Round 2 → Group 1
     * Round 3 → Group 1
     */
    public function generate(
        int $stageId
    ): array {
        $stage =
            $this->getStage(
                $stageId
            );

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

        if (
            $stage['status']
            !== 'pending'
        ) {
            throw new RuntimeException(
                'Battle Royale stage must be pending before generation.'
            );
        }

        /*
         * ==========================================
         * Room Size
         * ==========================================
         */
        $roomSize =
            (int) $stage['match_size'];

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

        if ($roomSize > 48) {
            throw new RuntimeException(
                'Battle Royale room size cannot exceed 48.'
            );
        }

        /*
         * ==========================================
         * Matches Per Player
         * ==========================================
         */
        $matchesPerPlayer =
            (int) $stage['matches_per_player'];

        if ($matchesPerPlayer < 1) {
            throw new RuntimeException(
                'Matches per player must be at least 1.'
            );
        }

        /*
         * ==========================================
         * Participants
         * ==========================================
         */
        $participants =
            $this->getParticipants(
                $stageId
            );

        $participantCount =
            count($participants);

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

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

        /*
         * ==========================================
         * تعداد Group
         * ==========================================
         *
         * ظرفیت واقعی Room حداکثر 48 است.
         *
         * مثال:
         *
         * 50  → 2 Group
         * 77  → 2 Group
         * 100 → 3 Group
         * 129 → 3 Group
         */
        $groupCount =
            (int) ceil(
                $participantCount /
                $roomSize
            );

        if ($groupCount < 1) {
            throw new RuntimeException(
                'Unable to calculate group count.'
            );
        }

        /*
         * ==========================================
         * Stage Type
         * ==========================================
         */
        $stageType =
            strtolower(
                trim(
                    (string) (
                        $stage['type'] ?? ''
                    )
                )
            );

        if (
            !in_array(
                $stageType,
                [
                    'qualification',
                    'group',
                    'playoff',
                    'final',
                ],
                true
            )
        ) {
            throw new RuntimeException(
                'Unsupported Battle Royale stage type.'
            );
        }

        /*
         * ==========================================
         * Seeded؟
         * ==========================================
         *
         * Qualification / Group:
         * Random
         *
         * Playoff / Final:
         * Seed Based
         */
        $isSeeded =
            in_array(
                $stageType,
                [
                    'playoff',
                    'final',
                ],
                true
            );

        /*
         * ==========================================
         * ساخت Groupها فقط یک بار
         * ==========================================
         */
        $groups =
            $isSeeded
                ? $this->buildSeededGroups(
                    $participants,
                    $groupCount
                )
                : $this->buildRandomGroups(
                    $participants,
                    $groupCount
                );

        /*
         * ==========================================
         * Validation
         * ==========================================
         */
        $this->validateGroups(
            $groups,
            $participants,
            $roomSize
        );

        /*
         * اندازه Groupها
         */
        $groupSizes =
            array_map(
                'count',
                $groups
            );

        $minGroupSize =
            min($groupSizes);

        $maxGroupSize =
            max($groupSizes);

        /*
         * ==========================================
         * Transaction
         * ==========================================
         */
        $ownsTransaction =
            !$this->pdo->inTransaction();

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

        try {

            $totalRooms = 0;

            /*
             * ======================================
             * ساخت Roomها
             *
             * نکته مهم:
             *
             * $groups هرگز داخل Round تغییر نمی‌کند.
             *
             * بنابراین:
             *
             * Round 1 / Room 1
             * Round 2 / Room 1
             * Round 3 / Room 1
             *
             * دقیقاً همان Group هستند.
             * ======================================
             */
            for (
                $round = 1;
                $round <= $matchesPerPlayer;
                $round++
            ) {

                foreach (
                    $groups
                    as $groupIndex =>
                    $groupParticipants
                ) {

                    /*
                     * شماره Group
                     *
                     * room_number = group number
                     */
                    $groupNumber =
                        $groupIndex + 1;

                    /*
                     * ساخت Room
                     */
                    $roomId =
                        $this->createRoom(
                            stageId:
                                $stageId,

                            roundNumber:
                                $round,

                            roomNumber:
                                $groupNumber,

                            capacity:
                                $roomSize,

                            participantCount:
                                count(
                                    $groupParticipants
                                )
                        );

                    /*
                     * ==================================
                     * Slot
                     * ==================================
                     */
                    $slot = 1;

                    foreach (
                        $groupParticipants
                        as $participant
                    ) {

                        $this->addRoomParticipant(
                            roomId:
                                $roomId,

                            participantId:
                                (int) (
                                    $participant['id']
                                ),

                            slot:
                                $slot
                        );

                        $slot++;
                    }

                    /*
                     * Room Ready
                     */
                    $this->markRoomReady(
                        $roomId
                    );

                    $totalRooms++;
                }
            }

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

            return [
                'stage_id' =>
                    $stageId,

                'participant_count' =>
                    $participantCount,

                'room_size' =>
                    $roomSize,

                'rounds' =>
                    $matchesPerPlayer,

                'groups' =>
                    $groupCount,

                'groups_min_size' =>
                    $minGroupSize,

                'groups_max_size' =>
                    $maxGroupSize,

                'total_rooms' =>
                    $totalRooms,

                'seeded' =>
                    $isSeeded,
            ];

        } catch (Throwable $e) {

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

            throw $e;
        }
    }

    /**
     * ============================================
     * Stage
     * ============================================
     */
    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;
    }

    /**
     * ============================================
     * Participants
     * ============================================
     *
     * فقط Participantهای active این Stage.
     *
     * Seed در Playoff / Final استفاده می‌شود.
     */
    private function getParticipants(
        int $stageId
    ): array {
        $statement =
            $this->pdo->prepare("
                SELECT
                    tp.id,
                    tp.event_player_id,
                    tsp.seed,
                    ep.user_id,
                    ep.ign

                FROM tournament_stage_participants tsp

                INNER JOIN tournament_participants tp
                    ON tp.id =
                        tsp.tournament_participant_id

                INNER JOIN event_players ep
                    ON ep.id =
                        tp.event_player_id

                WHERE tsp.stage_id = ?
                  AND tsp.status = 'active'

                ORDER BY
                    COALESCE(
                        tsp.seed,
                        999999
                    ),
                    tp.id ASC
            ");

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

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

    /**
     * ============================================
     * Random Groups
     * ============================================
     *
     * Group Assignment فقط یک بار انجام می‌شود.
     */
    private function buildRandomGroups(
        array $participants,
        int $groupCount
    ): array {
        if (
            empty($participants)
            || $groupCount < 1
        ) {
            return [];
        }

        $shuffled =
            $participants;

        /*
         * Random فقط یک بار.
         */
        shuffle(
            $shuffled
        );

        return $this->splitBalancedByGroupCount(
            $shuffled,
            $groupCount
        );
    }

    /**
     * ============================================
     * Seeded Groups
     * ============================================
     *
     * برای Playoff / Final.
     *
     * Seed پایین‌تر = رتبه بهتر.
     *
     * Snake Distribution:
     *
     * 1 → Group 1
     * 2 → Group 2
     * 3 → Group 2
     * 4 → Group 1
     * 5 → Group 1
     * 6 → Group 2
     *
     * این باعث می‌شود Seedهای قوی بین Groupها
     * پخش شوند.
     *
     * مهم:
     * Groupها فقط یک بار ساخته می‌شوند.
     */
    private function buildSeededGroups(
        array $participants,
        int $groupCount
    ): array {
        if (
            empty($participants)
            || $groupCount < 1
        ) {
            return [];
        }

        /*
         * مرتب‌سازی بر اساس Seed
         */
        usort(
            $participants,
            static function (
                array $a,
                array $b
            ): int {
                $seedA =
                    (int) (
                        $a['seed']
                        ?? PHP_INT_MAX
                    );

                $seedB =
                    (int) (
                        $b['seed']
                        ?? PHP_INT_MAX
                    );

                if (
                    $seedA ===
                    $seedB
                ) {
                    return
                        (
                            (int) $a['id']
                        )
                        <=>
                        (
                            (int) $b['id']
                        );
                }

                return
                    $seedA <=>
                    $seedB;
            }
        );

        /*
         * Groupها
         */
        $groups = [];

        for (
            $i = 0;
            $i < $groupCount;
            $i++
        ) {
            $groups[$i] = [];
        }

        /*
         * Snake
         */
        $groupIndex = 0;
        $direction = 1;

        foreach (
            $participants as $participant
        ) {

            $groups[$groupIndex][] =
                $participant;

            /*
             * یک Group
             */
            if (
                $groupCount === 1
            ) {
                continue;
            }

            /*
             * رسیدن به انتهای لیست
             */
            if (
                $groupIndex ===
                $groupCount - 1
            ) {
                $direction = -1;

            } elseif (
                $groupIndex === 0
            ) {
                $direction = 1;
            }

            $groupIndex +=
                $direction;
        }

        return array_values(
            $groups
        );
    }

    /**
     * ============================================
     * Balanced Split
     * ============================================
     *
     * اختلاف تعداد Groupها حداکثر 1 است.
     */
    private function splitBalancedByGroupCount(
        array $participants,
        int $groupCount
    ): array {
        $total =
            count($participants);

        if (
            $total === 0
            || $groupCount < 1
        ) {
            return [];
        }

        $base =
            intdiv(
                $total,
                $groupCount
            );

        $remainder =
            $total %
            $groupCount;

        $groups = [];

        $offset = 0;

        for (
            $i = 0;
            $i < $groupCount;
            $i++
        ) {

            $size =
                $base
                + (
                    $i < $remainder
                        ? 1
                        : 0
                );

            $groups[$i] =
                array_slice(
                    $participants,
                    $offset,
                    $size
                );

            $offset +=
                $size;
        }

        return $groups;
    }

    /**
     * ============================================
     * Validation
     * ============================================
     */
    private function validateGroups(
        array $groups,
        array $participants,
        int $roomSize
    ): void {
        if (empty($groups)) {
            throw new RuntimeException(
                'No groups were generated.'
            );
        }

        $totalExpected =
            count($participants);

        $totalActual = 0;

        $seen = [];

        foreach (
            $groups as $groupIndex =>
            $group
        ) {

            if (empty($group)) {
                throw new RuntimeException(
                    "Group #"
                    . ($groupIndex + 1)
                    . " is empty."
                );
            }

            /*
             * Group نباید از ظرفیت Room
             * بزرگ‌تر باشد.
             */
            if (
                count($group)
                >
                $roomSize
            ) {
                throw new RuntimeException(
                    "Group #"
                    . ($groupIndex + 1)
                    . " exceeds room capacity."
                );
            }

            foreach (
                $group as $participant
            ) {

                $participantId =
                    (int) (
                        $participant['id']
                        ?? 0
                    );

                if ($participantId <= 0) {
                    throw new RuntimeException(
                        'Invalid participant in group.'
                    );
                }

                /*
                 * هر Participant دقیقاً
                 * باید یک Group داشته باشد.
                 */
                if (
                    isset(
                        $seen[$participantId]
                    )
                ) {
                    throw new RuntimeException(
                        "Participant #"
                        . $participantId
                        . " appears in multiple groups."
                    );
                }

                $seen[$participantId] =
                    true;

                $totalActual++;
            }
        }

        /*
         * هیچ Participantی نباید
         * گم شده باشد.
         */
        if (
            $totalActual !==
            $totalExpected
        ) {
            throw new RuntimeException(
                'Group allocation participant count mismatch.'
            );
        }
    }

    /**
     * ============================================
     * جلوگیری از Generate مجدد
     * ============================================
     */
    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.'
            );
        }
    }

    /**
     * ============================================
     * ساخت Room
     * ============================================
     */
    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();
    }

    /**
     * ============================================
     * افزودن Participant به Room
     * ============================================
     */
    private function addRoomParticipant(
        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
        ]);
    }

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

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