<?php

declare(strict_types=1);

namespace Tournament\Bracket;

use PDO;
use RuntimeException;
use Throwable;

final class TournamentBracketGenerator
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly ByeHandler $byeHandler
    ) {
    }

    /**
     * Generate a Single Elimination bracket.
     *
     * @return array{
     *     bracket_id: int,
     *     participant_count: int,
     *     bracket_size: int,
     *     bye_count: int,
     *     rounds: int,
     *     matches: int,
     *     bye_processed: int,
     *     participants_advanced_by_bye: int
     * }
     */
public function generate(
    int $bracketId,
    int $stageId
): array {
    $ownsTransaction =
        !$this->pdo->inTransaction();

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

    error_log(
    '[BRACKET DEBUG] Generator transaction started: ' .
    ($this->pdo->inTransaction() ? 'YES' : 'NO')
);

    try {
            /*
            * 1. Validate bracket.
            */
            $bracket = $this->getBracket(
                $bracketId,
                $stageId
            );

            if ($bracket['type'] !== 'single_elimination') {
                throw new RuntimeException(
                    'Only single elimination is supported.'
                );
            }

            if ($bracket['status'] !== 'pending') {
                throw new RuntimeException(
                    'Bracket is already generated or not pending.'
                );
            }

            /*
            * 2. Get tournament participants.
            */
            $participants = $this->getParticipants(
                $stageId
            );

            $participantCount =
                count($participants);

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

            /*
            * 3. Calculate bracket size.
            */
            $bracketSize =
                $this->nextPowerOfTwo(
                    $participantCount
                );

            $byeCount =
                $bracketSize - $participantCount;

            /*
            * 4. Assign seeds.
            */
            $this->assignSeeds(
                $participants
            );

            /*
            * Re-read participants after seeds.
            */
            $participants =
                $this->getParticipants(
                    $stageId
                );

            /*
            * 5. Build first-round pairings.
            */
            $pairings =
                $this->buildFirstRoundPairings(
                    $participants,
                    $bracketSize
                );

            /*
            * 6. Calculate rounds.
            */
            $rounds =
                (int) log(
                    $bracketSize,
                    2
                );

            /*
            * 7. Create all matches.
            */
            $matchIds =
                $this->createMatches(
                    bracketId: $bracketId,
                    stageId: $stageId,
                    rounds: $rounds,
                    firstRoundPairings: $pairings
                );

            /*
            * 8. Connect matches.
            */
            $this->connectMatches(
                bracketId: $bracketId,
                rounds: $rounds,
                matchIds: $matchIds
            );

            /*
            * 9. Process BYEs.
            *
            * ByeHandler does NOT start/commit
            * a transaction.
            */
            $byeResult =
                $this->byeHandler->processBracket(
                    $bracketId
                );

            /*
            * 10. Activate bracket.
            */
            $this->activateBracket(
                bracketId: $bracketId,
                participantCount: $participantCount
            );

            /*
            * Commit only when Generator owns
            * the transaction.
            */
            if ($ownsTransaction) {
                $this->pdo->commit();
            }

            return [
                'bracket_id' =>
                    $bracketId,

                'participant_count' =>
                    $participantCount,

                'bracket_size' =>
                    $bracketSize,

                'bye_count' =>
                    $byeCount,

                'rounds' =>
                    $rounds,

                'matches' =>
                    $this->countMatches(
                        $matchIds
                    ),

                'bye_processed' =>
                    $byeResult['processed'],

                'participants_advanced_by_bye' =>
                    $byeResult['participants_advanced'],
            ];

        } catch (Throwable $e) {

            error_log(
                '[BRACKET ERROR] ' .
                get_class($e) .
                ': ' .
                $e->getMessage()
            );

            error_log(
                '[BRACKET ERROR FILE] ' .
                $e->getFile() .
                ':' .
                $e->getLine()
            );

            error_log(
                "[BRACKET ERROR TRACE]\n" .
                $e->getTraceAsString()
            );

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

            throw $e;
        }
    }

    /**
     * Get bracket and validate its stage.
     */
    private function getBracket(
        int $bracketId,
        int $stageId
    ): array {
        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                id,
                stage_id,
                type,
                status,
                participant_count
            FROM tournament_brackets
            WHERE id = ?
              AND stage_id = ?
            LIMIT 1
            SQL
        );

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

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

        if ($bracket === false) {
            throw new RuntimeException(
                'Bracket not found.'
            );
        }

        return $bracket;
    }

    /**
     * Get active tournament participants.
     */
    private function getParticipants(
        int $stageId
    ): array {
        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                tp.id,
                tp.event_player_id,
                tp.seed,
                tp.status
            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.seed IS NULL,
                tp.seed ASC,
                tp.id ASC
            SQL
        );

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

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

    /**
     * Assign sequential seeds to participants
     * that don't already have one.
     */
    private function assignSeeds(
        array $participants
    ): void {
        /*
         * Important:
         * Existing seeds must not be overwritten.
         */
        $usedSeeds = [];

        foreach ($participants as $participant) {
            if ($participant['seed'] !== null) {
                $usedSeeds[
                    (int) $participant['seed']
                ] = true;
            }
        }

        $nextSeed = 1;

        foreach ($participants as $participant) {
            if ($participant['seed'] !== null) {
                continue;
            }

            while (isset($usedSeeds[$nextSeed])) {
                $nextSeed++;
            }

            $statement = $this->pdo->prepare(
                <<<SQL
                UPDATE tournament_participants
                SET seed = ?
                WHERE id = ?
                SQL
            );

            $statement->execute([
                $nextSeed,
                $participant['id'],
            ]);

            $usedSeeds[$nextSeed] = true;

            $nextSeed++;
        }
    }

    /**
     * Find the nearest power of two.
     */
    private function nextPowerOfTwo(
        int $number
    ): int {
        $power = 1;

        while ($power < $number) {
            $power *= 2;
        }

        return $power;
    }

    /**
     * Build first-round pairings.
     */
    private function buildFirstRoundPairings(
        array $participants,
        int $bracketSize
    ): array {
        $seedMap = [];

        foreach ($participants as $participant) {
            $seedMap[
                (int) $participant['seed']
            ] = $participant;
        }

        $seedOrder = $this->generateSeedOrder(
            $bracketSize
        );

        $pairings = [];

        for (
            $index = 0;
            $index < count($seedOrder);
            $index += 2
        ) {
            $seedA = $seedOrder[$index];
            $seedB = $seedOrder[$index + 1];

            $pairings[] = [
                'a' => $seedMap[$seedA] ?? null,
                'b' => $seedMap[$seedB] ?? null,
            ];
        }

        return $pairings;
    }

    /**
     * Generate standard tournament seed order.
     */
    private function generateSeedOrder(
        int $size
    ): array {
        if ($size === 1) {
            return [1];
        }

        $order = [1, 2];

        while (count($order) < $size) {
            $nextSize = count($order) * 2;

            $newOrder = [];

            foreach ($order as $seed) {
                $newOrder[] = $seed;
                $newOrder[] =
                    $nextSize + 1 - $seed;
            }

            $order = $newOrder;
        }

        return $order;
    }

    /**
     * Create all bracket matches.
     */
    private function createMatches(
        int $bracketId,
        int $stageId,
        int $rounds,
        array $firstRoundPairings
    ): array {
        $matchIds = [];

        $matchNumber = 1;

        /*
         * Round 1.
         */
        foreach ($firstRoundPairings as $pairing) {
            $isBye =
                ($pairing['a'] === null)
                xor
                ($pairing['b'] === null);

            $statement = $this->pdo->prepare(
                <<<SQL
                INSERT INTO tournament_matches (
                    bracket_id,
                    stage_id,
                    round_number,
                    match_number,
                    status,
                    is_bye
                )
                VALUES (?, ?, 1, ?, ?, ?)
                SQL
            );

            $status = $isBye
                ? 'ready'
                : 'pending';

            $statement->execute([
                $bracketId,
                $stageId,
                $matchNumber,
                $status,
                $isBye ? 1 : 0,
            ]);

            $matchId = (int) $this->pdo->lastInsertId();

            $matchIds[1][] = $matchId;

            /*
             * Slot A.
             */
            if ($pairing['a'] !== null) {
                $this->addMatchParticipant(
                    $matchId,
                    (int) $pairing['a']['id'],
                    1
                );
            }

            /*
             * Slot B.
             */
            if ($pairing['b'] !== null) {
                $this->addMatchParticipant(
                    $matchId,
                    (int) $pairing['b']['id'],
                    2
                );
            }

            $matchNumber++;
        }

        /*
         * Remaining rounds.
         */
        for (
            $round = 2;
            $round <= $rounds;
            $round++
        ) {
            $previousMatches =
                $matchIds[$round - 1];

            $matchIds[$round] = [];

            $count = count($previousMatches);

            for (
                $index = 0;
                $index < $count;
                $index += 2
            ) {
                $statement = $this->pdo->prepare(
                    <<<SQL
                    INSERT INTO tournament_matches (
                        bracket_id,
                        stage_id,
                        round_number,
                        match_number,
                        status,
                        is_bye
                    )
                    VALUES (?, ?, ?, ?, 'pending', 0)
                    SQL
                );

                $statement->execute([
                    $bracketId,
                    $stageId,
                    $round,
                    $matchNumber,
                ]);

                $matchIds[$round][] =
                    (int) $this->pdo->lastInsertId();

                $matchNumber++;
            }
        }

        return $matchIds;
    }

    /**
     * Add participant to match.
     */
    private function addMatchParticipant(
        int $matchId,
        int $participantId,
        int $slot
    ): void {
        $statement = $this->pdo->prepare(
            <<<SQL
            INSERT INTO tournament_match_participants (
                match_id,
                tournament_participant_id,
                slot,
                status
            )
            VALUES (?, ?, ?, 'pending')
            SQL
        );

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

    /**
     * Connect matches to their next round.
     */
    private function connectMatches(
        int $bracketId,
        int $rounds,
        array $matchIds
    ): void {
        for (
            $round = 1;
            $round < $rounds;
            $round++
        ) {
            $currentRound =
                $matchIds[$round];

            $nextRound =
                $matchIds[$round + 1];

            foreach (
                $currentRound as $index => $matchId
            ) {
                $nextMatchIndex =
                    intdiv($index, 2);

                $nextMatchId =
                    $nextRound[$nextMatchIndex];

                $slot =
                    ($index % 2 === 0)
                        ? 'a'
                        : 'b';

                $statement = $this->pdo->prepare(
                    <<<SQL
                    UPDATE tournament_matches
                    SET
                        winner_next_match_id = ?,
                        winner_next_slot = ?
                    WHERE id = ?
                      AND bracket_id = ?
                    SQL
                );

                $statement->execute([
                    $nextMatchId,
                    $slot,
                    $matchId,
                    $bracketId,
                ]);
            }
        }
    }

    /**
     * Activate bracket after successful generation.
     */
    private function activateBracket(
        int $bracketId,
        int $participantCount
    ): void {
        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_brackets
            SET
                participant_count = ?,
                status = 'active'
            WHERE id = ?
              AND status = 'pending'
            SQL
        );

        $statement->execute([
            $participantCount,
            $bracketId,
        ]);

        if ($statement->rowCount() !== 1) {
            throw new RuntimeException(
                'Failed to activate bracket.'
            );
        }
    }

    /**
     * Count all generated matches.
     */
    private function countMatches(
        array $matchIds
    ): int {
        $count = 0;

        foreach ($matchIds as $roundMatches) {
            $count += count($roundMatches);
        }

        return $count;
    }
}