<?php

declare(strict_types=1);

namespace Tournament\Bracket;

use PDO;
use RuntimeException;

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

    /**
     * Advance a finished match winner.
     *
     * Transaction is managed by the caller.
     *
     * @return array{
     *     match_id: int,
     *     winner_participant_id: int,
     *     next_match_id: int|null,
     *     next_slot: string|null,
     *     tournament_completed: bool
     * }
     */
    public function advanceWinner(
        int $matchId,
        int $winnerParticipantId
    ): array {
        $match = $this->getMatchForUpdate($matchId);

        $this->validateWinner(
            $match,
            $winnerParticipantId
        );

        /*
         * Final match.
         */
        if ($match['winner_next_match_id'] === null) {
            $this->finishBracket(
                (int) $match['bracket_id'],
                $winnerParticipantId
            );

            return [
                'match_id' =>
                    $matchId,

                'winner_participant_id' =>
                    $winnerParticipantId,

                'next_match_id' =>
                    null,

                'next_slot' =>
                    null,

                'tournament_completed' =>
                    true,
            ];
        }

        $nextMatchId =
            (int) $match['winner_next_match_id'];

        $nextSlot =
            $match['winner_next_slot'];

        if ($nextSlot === null) {
            throw new RuntimeException(
                'Winner next slot is not defined.'
            );
        }

        $this->placeWinnerInNextMatch(
            $nextMatchId,
            $winnerParticipantId,
            $nextSlot
        );

        $this->updateNextMatchStatus(
            $nextMatchId
        );

        return [
            'match_id' =>
                $matchId,

            'winner_participant_id' =>
                $winnerParticipantId,

            'next_match_id' =>
                $nextMatchId,

            'next_slot' =>
                $nextSlot,

            'tournament_completed' =>
                false,
        ];
    }

    /**
     * Get match and lock it.
     */
    private function getMatchForUpdate(
        int $matchId
    ): array {
        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                id,
                bracket_id,
                stage_id,
                status,
                winner_participant_id,
                winner_next_match_id,
                winner_next_slot
            FROM tournament_matches
            WHERE id = ?
            LIMIT 1
            FOR UPDATE
            SQL
        );

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

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

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

        return $match;
    }

    /**
     * Validate winner.
     */
    private function validateWinner(
        array $match,
        int $winnerParticipantId
    ): void {
        if ($winnerParticipantId <= 0) {
            throw new RuntimeException(
                'Invalid winner participant ID.'
            );
        }

        if ($match['status'] !== 'finished') {
            throw new RuntimeException(
                'Match must be finished before advancing its winner.'
            );
        }

        if (
            (int) $match['winner_participant_id']
            !== $winnerParticipantId
        ) {
            throw new RuntimeException(
                'Winner does not match match result.'
            );
        }
    }

    /**
     * Place winner into the next match.
     */
    private function placeWinnerInNextMatch(
        int $nextMatchId,
        int $winnerParticipantId,
        string $slot
    ): void {
        if (!in_array($slot, ['a', 'b'], true)) {
            throw new RuntimeException(
                'Invalid next match slot.'
            );
        }

        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                id,
                status
            FROM tournament_matches
            WHERE id = ?
            LIMIT 1
            FOR UPDATE
            SQL
        );

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

        if ($statement->fetch(PDO::FETCH_ASSOC) === false) {
            throw new RuntimeException(
                'Next match not found.'
            );
        }

        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                id,
                tournament_participant_id
            FROM tournament_match_participants
            WHERE match_id = ?
              AND slot = ?
            LIMIT 1
            FOR UPDATE
            SQL
        );

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

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

        if ($existing !== false) {
            if (
                (int) $existing['tournament_participant_id']
                === $winnerParticipantId
            ) {
                return;
            }

            throw new RuntimeException(
                "Next match slot '{$slot}' is already occupied."
            );
        }

        /*
         * Prevent the same participant from
         * entering the same match twice.
         */
        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT id
            FROM tournament_match_participants
            WHERE match_id = ?
              AND tournament_participant_id = ?
            LIMIT 1
            FOR UPDATE
            SQL
        );

        $statement->execute([
            $nextMatchId,
            $winnerParticipantId,
        ]);

        if ($statement->fetchColumn() !== false) {
            throw new RuntimeException(
                'Participant already exists in the next match.'
            );
        }

        $statement = $this->pdo->prepare(
            <<<SQL
            INSERT INTO tournament_match_participants (
                match_id,
                tournament_participant_id,
                slot,
                status
            )
            VALUES (?, ?, ?, 'pending')
            SQL
        );

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

    /**
     * Update next match status.
     */
    private function updateNextMatchStatus(
        int $matchId
    ): void {
        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                COUNT(*)
            FROM tournament_match_participants
            WHERE match_id = ?
            SQL
        );

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

        $count = (int) $statement->fetchColumn();

        if ($count < 2) {
            return;
        }

        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_matches
            SET status = 'ready'
            WHERE id = ?
              AND status = 'pending'
            SQL
        );

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

        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_match_participants
            SET status = 'ready'
            WHERE match_id = ?
              AND status = 'pending'
            SQL
        );

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

    /**
     * Complete the bracket.
     */
    private function finishBracket(
        int $bracketId,
        int $winnerParticipantId
    ): void {
        /*
         * Mark tournament participant as winner.
         */
        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_participants
            SET status = 'winner'
            WHERE id = ?
              AND status = 'active'
            SQL
        );

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

        if ($statement->rowCount() !== 1) {
            throw new RuntimeException(
                'Unable to mark final winner.'
            );
        }

        /*
         * Complete bracket.
         */
        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_brackets
            SET status = 'completed'
            WHERE id = ?
              AND status = 'active'
            SQL
        );

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