<?php

declare(strict_types=1);

namespace Tournament\Stage;

use PDO;
use RuntimeException;
use Throwable;

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

    /**
     * Generate Battle Royale Leaderboard.
     *
     * معماری:
     *
     * - room_number = Group
     * - Group در تمام Roundها ثابت است.
     * - Score تمام Roundهای یک Group جمع می‌شود.
 * - rank_position = رتبه داخل Group
     * - global_rank   = رتبه کل Stage
     *
     * قوانین DQ:
     *
     * - اگر بازیکن در تمام Roundهای Stage
     *   placement = 0 داشته باشد:
     *      global_rank = NULL
     *
     * - اگر حداقل یک Round دارای
     *   placement > 0 باشد:
     *      بازیکن Eligible است.
     *
     * qualified در این کلاس محاسبه نمی‌شود.
     * تعیین Qualified فقط در BattleRoyaleAdvancement انجام می‌شود.
     */
    public function generate(
        int $stageId
    ): array {

        /*
         * ==========================================
         * Stage
         * ==========================================
         */
        $stage =
            $this->getStage(
                $stageId
            );

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

        /*
         * ==========================================
         * Rooms
         * ==========================================
         */
        $roomStats =
            $this->getRoomStats(
                $stageId
            );

        $totalRooms =
            $roomStats['total_rooms'];

        $completedRooms =
            $roomStats['completed_rooms'];

        if ($totalRooms < 1) {
            throw new RuntimeException(
                'هیچ Roomای برای این Stage وجود ندارد.'
            );
        }

        if (
            $completedRooms
            !==
            $totalRooms
        ) {
            throw new RuntimeException(
                "هنوز همه Roomهای Stage تکمیل نشده‌اند. "
                . "("
                . $completedRooms
                . "/"
                . $totalRooms
                . ")"
            );
        }

        /*
         * ==========================================
         * Stage Configuration
         * ==========================================
         */
        $matchesPerPlayer =
            (int) (
                $stage['matches_per_player']
                ?? 0
            );

        if (
            $matchesPerPlayer < 1
        ) {
            throw new RuntimeException(
                'تعداد بازی‌های Stage نامعتبر است.'
            );
        }

        /*
         * ==========================================
         * Results
         * ==========================================
         */
        $results =
            $this->getResults(
                $stageId
            );

        if (!$results) {
            throw new RuntimeException(
                'هیچ نتیجه تاییدشده‌ای برای این Stage پیدا نشد.'
            );
        }

        /*
         * ==========================================
         * ساخت Groupها
         * ==========================================
         */
        $groups = [];

        foreach (
            $results as $result
        ) {

            $groupNumber =
                (int) (
                    $result['group_number']
                    ?? 0
                );

            $participantId =
                (int) (
                    $result[
                        'tournament_participant_id'
                    ]
                    ?? 0
                );

            if (
                $groupNumber <= 0
            ) {
                throw new RuntimeException(
                    'Group number نامعتبر است.'
                );
            }

            if (
                $participantId <= 0
            ) {
                throw new RuntimeException(
                    'Tournament participant نامعتبر است.'
                );
            }

            if (
                !isset(
                    $groups[$groupNumber]
                )
            ) {
                $groups[$groupNumber] = [];
            }

            if (
                !isset(
                    $groups[
                        $groupNumber
                    ][$participantId]
                )
            ) {

                $groups[
                    $groupNumber
                ][$participantId] = [

                    'tournament_participant_id' =>
                        $participantId,

                    'total_score' =>
                        0.0,

                    'total_kills' =>
                        0,

                    'games_played' =>
                        0,

                    'best_placement' =>
                        null,

                    /*
                     * تعداد Roundهایی که
                     * placement = 0 بوده
                     */
                    'zero_placements' =>
                        0,

                    /*
                     * تعداد Roundهایی که
                     * placement > 0 بوده
                     */
                    'valid_placements' =>
                        0,

                    /*
                     * بعداً پر می‌شود.
                     */
                    'rank_position' =>
                        null,

                    /*
                     * بعداً پر می‌شود.
                     */
                    'global_rank' =>
                        null,
                ];
            }

            /*
             * ==========================================
             * جمع Score
             * ==========================================
             */
            $groups[
                $groupNumber
            ][$participantId][
                'total_score'
            ] +=
                (float) (
                    $result['score']
                    ?? 0
                );

            /*
             * ==========================================
             * جمع Kills
             * ==========================================
             */
            $groups[
                $groupNumber
            ][$participantId][
                'total_kills'
            ] +=
                (int) (
                    $result['kills']
                    ?? 0
                );

            /*
             * ==========================================
             * تعداد بازی
             * ==========================================
             */
            $groups[
                $groupNumber
            ][$participantId][
                'games_played'
            ]++;

            /*
             * ==========================================
             * Placement
             * ==========================================
             */
            $placement =
                (int) (
                    $result['placement']
                    ?? 0
                );

            /*
             * placement = 0
             * یعنی این Round DQ / عدم حضور
             */
            if (
                $placement === 0
            ) {

                $groups[
                    $groupNumber
                ][$participantId][
                    'zero_placements'
                ]++;

            /*
             * placement > 0
             * یعنی Placement معتبر
             */
            } elseif (
                $placement > 0
            ) {

                $groups[
                    $groupNumber
                ][$participantId][
                    'valid_placements'
                ]++;

                /*
                 * بهترین Placement
                 */
                if (
                    $groups[
                        $groupNumber
                    ][$participantId][
                        'best_placement'
                    ] === null

                    ||

                    $placement <
                    $groups[
                        $groupNumber
                    ][$participantId][
                        'best_placement'
                    ]
                ) {

                    $groups[
                        $groupNumber
                    ][$participantId][
                        'best_placement'
                    ] =
                        $placement;
                }
            }
        }

        /*
         * ==========================================
         * Validation تعداد بازی
         * ==========================================
         *
         * هر Participant باید دقیقاً به تعداد
         * matches_per_player نتیجه داشته باشد.
         */
        foreach (
            $groups as $groupNumber =>
            $players
        ) {

            foreach (
                $players as $participantId =>
                $player
            ) {

                if (
                    $player['games_played']
                    !==
                    $matchesPerPlayer
                ) {

                    throw new RuntimeException(
                        "Participant #{$participantId} "
                        . "در Group #{$groupNumber} "
                        . "فقط "
                        . $player['games_played']
                        . " بازی دارد، در حالی که باید "
                        . $matchesPerPlayer
                        . " بازی داشته باشد."
                    );
                }
            }
        }

        /*
         * ==========================================
         * Sort داخل هر Group
         * ==========================================
         *
         * 1. Total Score DESC
         * 2. Total Kills DESC
         * 3. Best Placement ASC
         * 4. Participant ID ASC
         *
         * توجه:
         * DQها فعلاً از Group حذف نمی‌شوند،
         * چون Leaderboard باید وضعیت آنها را نیز
         * نگه دارد.
         */
        foreach (
            $groups as $groupNumber =>
            &$players
        ) {

            $players =
                array_values(
                    $players
                );

            usort(
                $players,
                static function (
                    array $a,
                    array $b
                ): int {

                    /*
                     * Score DESC
                     */
                    $compare =
                        $b['total_score']
                        <=>
                        $a['total_score'];

                    if (
                        $compare !== 0
                    ) {
                        return $compare;
                    }

                    /*
                     * Kills DESC
                     */
                    $compare =
                        $b['total_kills']
                        <=>
                        $a['total_kills'];

                    if (
                        $compare !== 0
                    ) {
                        return $compare;
                    }

                    /*
                     * Best Placement ASC
                     */
                    $placementA =
                        $a['best_placement']
                        ??
                        PHP_INT_MAX;

                    $placementB =
                        $b['best_placement']
                        ??
                        PHP_INT_MAX;

                    $compare =
                        $placementA
                        <=>
                        $placementB;

                    if (
                        $compare !== 0
                    ) {
                        return $compare;
                    }

                    /*
                     * Participant ID ASC
                     */
                    return
                        $a[
                            'tournament_participant_id'
                        ]
                        <=>
                        $b[
                            'tournament_participant_id'
                        ];
                }
            );

            /*
             * ======================================
             * Rank داخل Group
             * ======================================
             */
            foreach (
                $players as $index =>
                &$player
            ) {

                $player['rank_position'] =
                    $index + 1;
            }

            unset($player);
        }

        unset($players);

        /*
         * ==========================================
         * Global Ranking
         * ==========================================
         *
         * فقط Eligibleها وارد Global Ranking می‌شوند.
         *
         * DQ واقعی:
         *
         * zero_placements === games_played
         *
         * یعنی تمام Roundها = 0
         *
         * بازیکنی که حتی یک placement > 0
         * داشته باشد، Eligible است.
         * ==========================================
         */
        $globalPlayers = [];

        foreach (
            $groups as $groupNumber =>
            $players
        ) {

            foreach (
                $players as $player
            ) {

                $gamesPlayed =
                    (int) (
                        $player['games_played']
                        ?? 0
                    );

                $zeroPlacements =
                    (int) (
                        $player['zero_placements']
                        ?? 0
                    );

                /*
                 * تمام Roundها صفر
                 * => DQ
                 */
                $isDisqualified =
                    $gamesPlayed > 0
                    &&
                    $zeroPlacements ===
                    $gamesPlayed;

                if (
                    $isDisqualified
                ) {

                    continue;
                }

                $globalPlayers[] =
                    $player;
            }
        }

        /*
         * ==========================================
         * Sort Global Ranking
         * ==========================================
         */
        usort(
            $globalPlayers,
            static function (
                array $a,
                array $b
            ): int {

                /*
                 * Score DESC
                 */
                $compare =
                    $b['total_score']
                    <=>
                    $a['total_score'];

                if (
                    $compare !== 0
                ) {
                    return $compare;
                }

                /*
                 * Kills DESC
                 */
                $compare =
                    $b['total_kills']
                    <=>
                    $a['total_kills'];

                if (
                    $compare !== 0
                ) {
                    return $compare;
                }

                /*
                 * Best Placement ASC
                 */
                $placementA =
                    $a['best_placement']
                    ??
                    PHP_INT_MAX;

                $placementB =
                    $b['best_placement']
                    ??
                    PHP_INT_MAX;

                $compare =
                    $placementA
                    <=>
                    $placementB;

                if (
                    $compare !== 0
                ) {
                    return $compare;
                }

                /*
                 * Participant ID ASC
                 */
                return
                    $a[
                        'tournament_participant_id'
                    ]
                    <=>
                    $b[
                        'tournament_participant_id'
                    ];
            }
        );

        /*
         * ==========================================
         * Assign Global Rank
         * ==========================================
         */
        foreach (
            $globalPlayers as $index =>
            $player
        ) {

            $participantId =
                (int) (
                    $player[
                        'tournament_participant_id'
                    ]
                );

            /*
             * پیدا کردن Participant
             * داخل Group خودش
             */
            foreach (
                $groups as &$groupPlayers
            ) {

                foreach (
                    $groupPlayers as
                    &$groupPlayer
                ) {

                    if (
                        (int) (
                            $groupPlayer[
                                'tournament_participant_id'
                            ]
                        )
                        !==
                        $participantId
                    ) {
                        continue;
                    }

                    $groupPlayer[
                        'global_rank'
                    ] =
                        $index + 1;

                    /*
                     * دیگر نیازی به ادامه
                     * جستجو نیست.
                     */
                    break 2;
                }

                unset($groupPlayer);
            }

            unset($groupPlayers);
        }

        /*
         * DQها global_rank = NULL باقی می‌مانند.
         *
         * این بخش فقط برای اطمینان است.
         */
        foreach (
            $groups as &$groupPlayers
        ) {

            foreach (
                $groupPlayers as &$player
            ) {

                $gamesPlayed =
                    (int) (
                        $player['games_played']
                        ?? 0
                    );

                $zeroPlacements =
                    (int) (
                        $player['zero_placements']
                        ?? 0
                    );

                if (
                    $gamesPlayed > 0
                    &&
                    $zeroPlacements ===
                    $gamesPlayed
                ) {

                    $player[
                        'global_rank'
                    ] =
                        null;
                }
            }

            unset($player);
        }

        unset($groupPlayers);

        /*
         * ==========================================
         * تعداد کل بازیکنان
         * ==========================================
         *
         * شامل DQها نیز می‌شود.
         */
        $totalPlayers =
            0;

        foreach (
            $groups as $players
        ) {

            $totalPlayers +=
                count($players);
        }

        /*
         * ==========================================
         * تعداد Eligible
         * ==========================================
         */
        $eligiblePlayers =
            count(
                $globalPlayers
            );

        /*
         * ==========================================
         * Group Validation
         * ==========================================
         */
        $groupNumbers =
            array_keys(
                $groups
            );

        sort(
            $groupNumbers,
            SORT_NUMERIC
        );

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

        if (
            $ownsTransaction
        ) {

            $this->pdo->beginTransaction();
        }

        try {

            /*
             * ======================================
             * حذف Leaderboard قبلی
             * ======================================
             */
            $delete =
                $this->pdo->prepare("
                    DELETE FROM tournament_br_leaderboards
                    WHERE stage_id = ?
                ");

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

            /*
             * ======================================
             * Insert
             * ======================================
             *
             * rank_position
             * = Rank داخل Group
             *
             * global_rank
             * = Rank کل Stage
             */
            $insert =
                $this->pdo->prepare("
                    INSERT INTO tournament_br_leaderboards (
                        stage_id,
                        tournament_participant_id,
                        total_score,
                        total_kills,
                        games_played,
                        best_placement,
                        rank_position,
                        global_rank,
                        qualified,
                        status
                    )
                    VALUES (
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        0,
                        'final'
                    )
                ");

            /*
             * ======================================
             * Insert به ترتیب Group
             * ======================================
             */
            foreach (
                $groupNumbers as $groupNumber
            ) {

                foreach (
                    $groups[
                        $groupNumber
                    ] as $player
                ) {

                    $globalRank =
                        $player[
                            'global_rank'
                        ];

                    /*
                     * DQ => NULL
                     */
                    if (
                        $globalRank !== null
                    ) {
                        $globalRank =
                            (int) $globalRank;
                    }

                    $insert->execute([

                        $stageId,

                        (int) (
                            $player[
                                'tournament_participant_id'
                            ]
                        ),

                        (float) (
                            $player[
                                'total_score'
                            ]
                        ),

                        (int) (
                            $player[
                                'total_kills'
                            ]
                        ),

                        (int) (
                            $player[
                                'games_played'
                            ]
                        ),

                        $player[
                            'best_placement'
                        ],

                        /*
                         * Group Rank
                         */
                        (int) (
                            $player[
                                'rank_position'
                            ]
                        ),

                        /*
                         * Global Rank
                         *
                         * DQ => NULL
                         */
                        $globalRank,
                    ]);
                }
            }

            /*
             * ======================================
             * Commit
             * ======================================
             */
            if (
                $ownsTransaction
            ) {

                $this->pdo->commit();
            }

        } catch (
            Throwable $e
        ) {

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

                $this->pdo->rollBack();
            }

            throw $e;
        }

        /*
         * ==========================================
         * Result
         * ==========================================
         */
        return [
            'stage_id' =>
                $stageId,

            /*
             * تعداد کل بازیکنان
             * شامل DQ
             */
            'participants' =>
                $totalPlayers,

            /*
             * تعداد Eligible
             */
            'eligible' =>
                $eligiblePlayers,

            'groups' =>
                count($groups),

            'rounds' =>
                $matchesPerPlayer,

            'leaderboard' =>
                $groups,
        ];
    }

    /**
     * ============================================
     * دریافت Resultهای تاییدشده
     * ============================================
     *
     * room_number = Group
     */
    private function getResults(
        int $stageId
    ): array {

        $statement =
            $this->pdo->prepare("
                SELECT
                    r.tournament_participant_id,

                    r.room_participant_id,

                    r.placement,
                    r.kills,
                    r.score,

                    br.id AS room_id,
                    br.round_number,
                    br.room_number AS group_number

                FROM tournament_br_results r

                INNER JOIN tournament_br_rooms br
                    ON br.id = r.room_id

                WHERE br.stage_id = ?
                  AND br.status = 'completed'
                  AND r.status = 'confirmed'

                ORDER BY
                    br.room_number ASC,
                    br.round_number ASC,
                    r.tournament_participant_id ASC
            ");

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

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

    /**
     * ============================================
     * Room Stats
     * ============================================
     */
    private function getRoomStats(
        int $stageId
    ): array {

        $statement =
            $this->pdo->prepare("
                SELECT
                    COUNT(*) AS total_rooms,

                    COALESCE(
                        SUM(
                            CASE
                                WHEN status = 'completed'
                                THEN 1
                                ELSE 0
                            END
                        ),
                        0
                    ) AS completed_rooms

                FROM tournament_br_rooms

                WHERE stage_id = ?
            ");

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

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

        return [
            'total_rooms' =>
                (int) (
                    $row[
                        'total_rooms'
                    ]
                    ?? 0
                ),

            'completed_rooms' =>
                (int) (
                    $row[
                        'completed_rooms'
                    ]
                    ?? 0
                ),
        ];
    }

    /**
     * ============================================
     * Stage
     * ============================================
     */
    private function getStage(
        int $stageId
    ): array {

        $statement =
            $this->pdo->prepare("
                SELECT
                    id,
                    format,
                    matches_per_player,
                    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;
    }
}