<?php

declare(strict_types=1);

namespace Tournament\Stage;

use PDO;
use RuntimeException;
use Throwable;

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

    /**
     * Process Battle Royale advancement.
     *
     * قوانین:
     *
     * - هر room_number یک Group است.
     * - Group هر Participant باید ثابت باشد.
     * - advancement_count برای top_n سقف/هدف کل Stage است.
     * - سهم هر Group توسط BattleRoyaleAdvancementCalculator
     *   محاسبه می‌شود.
     * - Participantهای disqualified واجد شرایط صعود نیستند.
     * - اگر مجموع افراد واجد شرایط کمتر از Top N باشد،
     *   فقط افراد موجود صعود می‌کنند.
     * - Seed مرحله بعد به صورت Round-Robin بین Groupها
     *   ساخته می‌شود.
     */
    public function process(
        int $stageId
    ): array {

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

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

        if (
            $stage['format']
            !==
            'battle_royale'
        ) {
            throw new RuntimeException(
                'Stage is not Battle Royale.'
            );
        }

        /*
         * ==========================================
         * Champion
         * ==========================================
         */
        if (
            $stage['advancement_type']
            ===
            'champion'
        ) {

            return [
                'stage_id' =>
                    $stageId,

                'total_players' =>
                    $this->getStageParticipantCount(
                        $stageId
                    ),

                'groups' =>
                    $this->getGroupCount(
                        $stageId
                    ),

                'advancement_type' =>
                    'champion',

                'requested_qualified' =>
                    0,

                'qualified_per_group' =>
                    0,

                'qualified' =>
                    0,
            ];
        }

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

        if (
            $roomStats['total_rooms'] < 1
        ) {
            throw new RuntimeException(
                'این Stage هیچ Roomای ندارد.'
            );
        }

        if (
            $roomStats['completed_rooms']
            !==
            $roomStats['total_rooms']
        ) {
            throw new RuntimeException(
                "همه Roomهای Stage هنوز تکمیل نشده‌اند. "
                . "("
                . $roomStats['completed_rooms']
                . "/"
                . $roomStats['total_rooms']
                . ")"
            );
        }

        /*
         * ==========================================
         * Group count
         * ==========================================
         */
        $groupCount =
            $this->getGroupCount(
                $stageId
            );

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

        /*
         * ==========================================
         * Stage participants
         * ==========================================
         */
        $totalPlayers =
            $this->getStageParticipantCount(
                $stageId
            );

        if (
            $totalPlayers < 1
        ) {
            throw new RuntimeException(
                'هیچ Participantی برای این Stage پیدا نشد.'
            );
        }

        /*
         * ==========================================
         * Leaderboard
         * ==========================================
         */
        $leaderboard =
            $this->getLeaderboard(
                $stageId
            );

        if (!$leaderboard) {
            throw new RuntimeException(
                'Leaderboard برای این Stage وجود ندارد.'
            );
        }

        /*
         * ==========================================
         * Participant -> Group
         * ==========================================
         */
        $participantGroups =
            $this->getParticipantGroups(
                $stageId
            );

        if (!$participantGroups) {
            throw new RuntimeException(
                'Group assignment برای این Stage پیدا نشد.'
            );
        }

        /*
         * ==========================================
         * Group Leaderboards
         * ==========================================
         */
        $groups = [];

        $stageDisqualifiedIds =
            $this->getStageDisqualifiedParticipantIds($stageId);

        foreach (
            $leaderboard as $row
        ) {

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

            /*
            * ==========================================
            * بازیکنی که در تمام Roundهای Stage
            * placement = 0 داشته، اصلاً وارد
            * Qualification نمی‌شود.
            * ==========================================
            */
            if (
                isset(
                    $stageDisqualifiedIds[$participantId]
                )
            ) {
                continue;
            }

            $groupNumber =
                (int) (
                    $participantGroups[
                        $participantId
                    ]
                    ?? 0
                );

            if (
                $groupNumber <= 0
            ) {
                throw new RuntimeException(
                    "Group برای Participant #{$participantId} پیدا نشد."
                );
            }

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

            $groups[$groupNumber][] = [
                'tournament_participant_id' =>
                    $participantId,

                'total_score' =>
                    (float) (
                        $row['total_score']
                        ?? 0
                    ),

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

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

                'best_placement' =>
                    $row['best_placement'] !== null
                        ? (int) $row['best_placement']
                        : null,

                /*
                * همه بازیکنان اینجا Eligible هستند،
                * چون DQهای Stage از قبل حذف شده‌اند.
                */
                'participant_status' =>
                    'active',
            ];
        }

        /*
         * ==========================================
         * Group validation
         * ==========================================
         */
        ksort(
            $groups,
            SORT_NUMERIC
        );

        if (
            count($groups)
            !==
            $groupCount
        ) {
            throw new RuntimeException(
                'تعداد Groupها با اطلاعات Roomها مطابقت ندارد.'
            );
        }

        /*
         * ==========================================
         * Rank داخل Group
         * ==========================================
         */
        foreach (
            $groups as &$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
                     */
                    return
                        $a[
                            'tournament_participant_id'
                        ]
                        <=>
                        $b[
                            'tournament_participant_id'
                        ];
                }
            );

            foreach (
                $players as $index =>
                &$player
            ) {

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

            unset($player);
        }

        unset($players);

        /*
         * ==========================================
         * Eligible Count هر Group
         * ==========================================
         *
         * disqualified ها از ظرفیت صعود حذف می‌شوند.
         */
        $groupEligibleCounts = [];

        foreach (
            $groups as $groupNumber => $players
        ) {
            $groupEligibleCounts[
                (int) $groupNumber
            ] = count($players);
        }

        /*
         * ==========================================
         * Advancement Calculator
         * ==========================================
         */
        $calculator =
            new BattleRoyaleAdvancementCalculator();

        $groupQuotas =
            $calculator->calculateGroupQuotas(
                $groupEligibleCounts,
                (string) (
                    $stage[
                        'advancement_type'
                    ]
                ),
                (int) (
                    $stage[
                        'advancement_count'
                    ]
                    ?? 0
                )
            );

        /*
         * ==========================================
         * Qualified Count واقعی
         * ==========================================
         */
        $requestedQualified =
            (int) (
                $stage[
                    'advancement_count'
                ]
                ?? 0
            );

        $qualifiedPerGroup =
            $groupQuotas;

        $qualifiedTotal =
            array_sum(
                $groupQuotas
            );

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

        if (
            $ownsTransaction
        ) {

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

        try {

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

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

            /*
             * ======================================
             * Insert
             * ======================================
             */
            $insert =
                $this->pdo->prepare("
                    INSERT INTO tournament_stage_advancements (
                        from_stage_id,
                        participant_id,
                        rank_position,
                        total_score,
                        status
                    )
                    VALUES (
                        ?,
                        ?,
                        ?,
                        ?,
                        'qualified'
                    )
                ");

            /*
             * ======================================
             * ساخت Qualifiedها + Seed
             * ======================================
             *
             * مثال:
             *
             * G1 Rank1 -> Seed 1
             * G2 Rank1 -> Seed 2
             * G3 Rank1 -> Seed 3
             *
             * G1 Rank2 -> Seed G+1
             * ...
             *
             * Groupهایی که سهم کمتر دارند
             * باعث خطا نمی‌شوند.
             */
            $qualifiedTotalActual =
                0;

            $groupNumbers =
                array_keys(
                    $groups
                );

            sort(
                $groupNumbers,
                SORT_NUMERIC
            );

            /*
             * تعداد حداکثر Rank
             */
            $maxQuota =
                !empty($groupQuotas)
                    ? max(
                        $groupQuotas
                    )
                    : 0;

            $nextSeed =
                1;

            for (
                $localRank = 1;
                $localRank <= $maxQuota;
                $localRank++
            ) {

                foreach (
                    $groupNumbers as $groupNumber
                ) {

                    $quota =
                        (int) (
                            $groupQuotas[
                                $groupNumber
                            ]
                            ?? 0
                        );

                    /*
                     * این Group در این Rank
                     * سهمیه ندارد.
                     */
                    if (
                        $localRank > $quota
                    ) {
                        continue;
                    }

                    if (
                        !isset(
                            $groups[
                                $groupNumber
                            ][
                                $localRank - 1
                            ]
                        )
                    ) {
                        continue;
                    }

                    $player =
                        $groups[
                            $groupNumber
                        ][
                            $localRank - 1
                        ];

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

                    /*
                     * Insert Advancement
                     */
                    $insert->execute([
                        $stageId,

                        $participantId,

                        $nextSeed,

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

                    $nextSeed++;

                    $qualifiedTotalActual++;
                }
            }

            /*
             * ======================================
             * کنترل
             * ======================================
             */
            if (
                $qualifiedTotalActual
                !==
                $qualifiedTotal
            ) {

                throw new RuntimeException(
                    'تعداد Qualified ایجادشده با سهمیه محاسبه‌شده مطابقت ندارد.'
                );
            }

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

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

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

                'total_players' =>
                    $totalPlayers,

                'groups' =>
                    $groupCount,

                'advancement_type' =>
                    (string) (
                        $stage[
                            'advancement_type'
                        ]
                    ),

                'requested_qualified' =>
                    $requestedQualified,

                /*
                 * برای سازگاری:
                 * اگر سهم گروه‌ها مساوی نباشد،
                 * این مقدار null است.
                 */
                'qualified_per_group' =>
                    $this->isUniformQuota(
                        $groupQuotas
                    )
                        ? (
                            (int) reset(
                                $groupQuotas
                            )
                        )
                        : null,

                /*
                 * سهم واقعی هر Group
                 */
                'group_quotas' =>
                    $groupQuotas,

                /*
                 * تعداد واقعی Qualified
                 */
                'qualified' =>
                    $qualifiedTotalActual,
            ];

        } catch (Throwable $e) {

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

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

            throw $e;
        }
    }

    /**
     * ============================================
     * بررسی یکسان بودن سهمیه‌ها
     * ============================================
     */
    private function isUniformQuota(
        array $quotas
    ): bool {

        if (!$quotas) {
            return true;
        }

        return
            count(
                array_unique(
                    array_map(
                        'intval',
                        $quotas
                    )
                )
            )
            ===
            1;
    }

    /**
     * ============================================
     * Participant Status
     * ============================================
     */
    private function getParticipantStatus(
        int $participantId
    ): string {

        $statement =
            $this->pdo->prepare("
                SELECT
                    status
                FROM tournament_participants
                WHERE id = ?
                LIMIT 1
            ");

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

        return (string) (
            $statement->fetchColumn()
            ?: 'active'
        );
    }

    /**
     * ============================================
     * Leaderboard
     * ============================================
     */
    private function getLeaderboard(
        int $stageId
    ): array {

        $statement =
            $this->pdo->prepare("
                SELECT
                    tournament_participant_id,
                    total_score,
                    total_kills,
                    games_played,
                    best_placement,
                    rank_position

                FROM tournament_br_leaderboards

                WHERE stage_id = ?

                ORDER BY
                    rank_position ASC,
                    tournament_participant_id ASC
            ");

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

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

    /**
     * ============================================
     * Participant -> Group
     * ============================================
     */
    private function getParticipantGroups(
        int $stageId
    ): array {

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

                    MIN(
                        br.room_number
                    ) AS group_number,

                    COUNT(
                        DISTINCT br.room_number
                    ) AS group_count

                FROM tournament_br_room_participants rp

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

                WHERE br.stage_id = ?

                GROUP BY
                    rp.tournament_participant_id

                HAVING
                    group_count = 1
            ");

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

        $rows =
            $statement->fetchAll(
                PDO::FETCH_ASSOC
            );

        $groups = [];

        foreach (
            $rows as $row
        ) {

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

            $groupNumber =
                (int) (
                    $row[
                        'group_number'
                    ]
                );

            if (
                $participantId <= 0
                ||
                $groupNumber <= 0
            ) {
                continue;
            }

            $groups[
                $participantId
            ] =
                $groupNumber;
        }

        return $groups;
    }

    /**
     * ============================================
     * Group Count
     * ============================================
     */
    private function getGroupCount(
        int $stageId
    ): int {

        $statement =
            $this->pdo->prepare("
                SELECT
                    COUNT(
                        DISTINCT room_number
                    )

                FROM tournament_br_rooms

                WHERE stage_id = ?
            ");

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

        return (int)
            $statement->fetchColumn();
    }

    /**
     * ============================================
     * Participant Count
     * ============================================
     */
    private function getStageParticipantCount(
        int $stageId
    ): int {

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

                FROM tournament_stage_participants

                WHERE stage_id = ?

                  AND status IN (
                      'active',
                      'winner',
                      'eliminated',
                      'disqualified'
                  )
            ");

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

        return (int)
            $statement->fetchColumn();
    }

    /**
     * ============================================
     * 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,
                    advancement_type,
                    advancement_count,
                    status

                FROM tournament_stages

                WHERE id = ?

                LIMIT 1
            ");

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

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

        return
            $stage ?: null;
    }

    private function getStageDisqualifiedParticipantIds(
        int $stageId
    ): array {

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

                COUNT(DISTINCT br.id) AS total_rounds,

                SUM(
                    CASE
                        WHEN r.placement = 0
                        THEN 1
                        ELSE 0
                    END
                ) AS zero_rounds

            FROM tournament_br_rooms br

            INNER JOIN tournament_br_room_participants rp
                ON rp.room_id = br.id

            INNER JOIN tournament_br_results r
                ON r.room_participant_id = rp.id
            AND r.tournament_participant_id =
                rp.tournament_participant_id

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

            GROUP BY
                rp.tournament_participant_id

            HAVING
                total_rounds > 0
                AND zero_rounds = total_rounds
        ");

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

        $rows =
            $statement->fetchAll(
                PDO::FETCH_COLUMN
            );

        $disqualified = [];

        foreach ($rows as $participantId) {

            $participantId =
                (int) $participantId;

            if ($participantId > 0) {

                $disqualified[
                    $participantId
                ] = true;
            }
        }

        return $disqualified;
    }
}