<?php

declare(strict_types=1);

namespace Tournament\Bracket;

use PDO;
use RuntimeException;
use Throwable;

final class BracketManager
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly TournamentBracketGenerator $generator,
        private readonly BracketValidator $validator,
        private readonly ByeHandler $byeHandler,
        private readonly MatchResultService $matchResultService
    ) {
    }

    /**
     * Get bracket information.
     */
    public function get(
        int $bracketId
    ): array {
        if ($bracketId <= 0) {
            throw new RuntimeException(
                'Invalid bracket ID.'
            );
        }

        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                id,
                stage_id,
                name,
                type,
                status,
                participant_count,
                created_at,
                updated_at
            FROM tournament_brackets
            WHERE id = ?
            LIMIT 1
            SQL
        );

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

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

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

        return $bracket;
    }

    /**
     * Generate a bracket.
     *
     * The generator handles:
     * - participant loading
     * - seeding
     * - match creation
     * - match connections
     *
     * Then BYEs are processed.
     *
     * @return array{
     *     bracket_id: int,
     *     participant_count: int,
     *     bracket_size: int,
     *     bye_count: int,
     *     rounds: int,
     *     matches: int
     * }
     */
    public function generate(
        int $bracketId,
        int $stageId
    ): array {
        $this->validator->validateBracket(
            $bracketId,
            $stageId
        );

        $result = $this->generator->generate(
            $bracketId,
            $stageId
        );

        /*
         * BYEs are processed after the generator
         * commits its transaction.
         */
        if ($result['bye_count'] > 0) {
            $this->byeHandler->processBracket(
                $bracketId
            );
        }

        return $result;
    }

    /**
     * Get all matches of a bracket.
     *
     * @return array<int, array<string, mixed>>
     */
    public function getMatches(
        int $bracketId
    ): array {
        $this->get($bracketId);

        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                tm.id,
                tm.bracket_id,
                tm.stage_id,
                tm.round_number,
                tm.match_number,
                tm.status,
                tm.scheduled_at,
                tm.started_at,
                tm.finished_at,
                tm.winner_participant_id,
                tm.winner_next_match_id,
                tm.winner_next_slot,
                tm.is_bye
            FROM tournament_matches tm
            WHERE tm.bracket_id = ?
            ORDER BY
                tm.round_number ASC,
                tm.match_number ASC
            SQL
        );

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

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

        if ($matches === []) {
            return [];
        }

        /*
         * Load participants for all matches
         * in one query instead of N+1 queries.
         */
        $matchIds = array_map(
            static fn (array $match): int =>
                (int) $match['id'],
            $matches
        );

        $placeholders = implode(
            ',',
            array_fill(
                0,
                count($matchIds),
                '?'
            )
        );

        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                tmp.id,
                tmp.match_id,
                tmp.tournament_participant_id,
                tmp.slot,
                tmp.status,
                tp.event_player_id,
                tp.seed
            FROM tournament_match_participants tmp
            INNER JOIN tournament_participants tp
                ON tp.id = tmp.tournament_participant_id
            WHERE tmp.match_id IN ($placeholders)
            ORDER BY
                tmp.match_id ASC,
                tmp.slot ASC
            SQL
        );

        $statement->execute(
            $matchIds
        );

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

        $participantsByMatch = [];

        foreach ($participants as $participant) {
            $matchId =
                (int) $participant['match_id'];

            $participantsByMatch[$matchId][] =
                $participant;
        }

        foreach ($matches as &$match) {
            $matchId =
                (int) $match['id'];

            $match['participants'] =
                $participantsByMatch[$matchId] ?? [];
        }

        unset($match);

        return $matches;
    }

    /**
     * Get matches of a specific round.
     *
     * @return array<int, array<string, mixed>>
     */
    public function getRoundMatches(
        int $bracketId,
        int $roundNumber
    ): array {
        if ($roundNumber <= 0) {
            throw new RuntimeException(
                'Invalid round number.'
            );
        }

        $this->get($bracketId);

        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                id,
                bracket_id,
                stage_id,
                round_number,
                match_number,
                status,
                scheduled_at,
                started_at,
                finished_at,
                winner_participant_id,
                winner_next_match_id,
                winner_next_slot,
                is_bye
            FROM tournament_matches
            WHERE bracket_id = ?
              AND round_number = ?
            ORDER BY match_number ASC
            SQL
        );

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

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

    /**
     * Start a match.
     */
    public function startMatch(
        int $matchId
    ): void {
        $this->pdo->beginTransaction();

        try {
            $statement = $this->pdo->prepare(
                <<<SQL
                SELECT
                    id,
                    status,
                    is_bye
                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.'
                );
            }

            if ((int) $match['is_bye'] === 1) {
                throw new RuntimeException(
                    'A BYE match cannot be started manually.'
                );
            }

            if ($match['status'] !== 'ready') {
                throw new RuntimeException(
                    'Only ready matches can be started.'
                );
            }

            $statement = $this->pdo->prepare(
                <<<SQL
                UPDATE tournament_matches
                SET
                    status = 'live',
                    started_at = COALESCE(
                        started_at,
                        NOW()
                    )
                WHERE id = ?
                  AND status = 'ready'
                SQL
            );

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

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

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

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

            $this->pdo->commit();
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            throw $e;
        }
    }

    /**
     * Submit a match result.
     *
     * @param array<int, array{
     *     participant_id: int,
     *     placement?: int|null,
     *     kills?: int|null,
     *     score?: float|int|null
     * }> $results
     *
     * @return array<string, mixed>
     */
    public function submitResult(
        int $matchId,
        array $results,
        int $submittedBy
    ): array {
        return $this->matchResultService->submit(
            $matchId,
            $results,
            $submittedBy
        );
    }

    /**
     * Process pending BYEs manually.
     *
     * Useful for recovery / admin tools.
     *
     * @return array{
     *     processed: int,
     *     participants_advanced: int
     * }
     */
    public function processByes(
        int $bracketId
    ): array {
        $bracket = $this->get(
            $bracketId
        );

        if (
            !in_array(
                $bracket['status'],
                ['active'],
                true
            )
        ) {
            throw new RuntimeException(
                'Only active brackets can process BYEs.'
            );
        }

        return $this->byeHandler->processBracket(
            $bracketId
        );
    }

    /**
     * Cancel bracket.
     */
    public function cancel(
        int $bracketId
    ): void {
        $this->pdo->beginTransaction();

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

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

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

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

            if (
                in_array(
                    $bracket['status'],
                    ['completed', 'cancelled'],
                    true
                )
            ) {
                throw new RuntimeException(
                    'Bracket is already closed.'
                );
            }

            $statement = $this->pdo->prepare(
                <<<SQL
                UPDATE tournament_brackets
                SET status = 'cancelled'
                WHERE id = ?
                SQL
            );

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

            $statement = $this->pdo->prepare(
                <<<SQL
                UPDATE tournament_matches
                SET status = 'cancelled'
                WHERE bracket_id = ?
                  AND status NOT IN (
                      'finished',
                      'cancelled'
                  )
                SQL
            );

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

            $this->pdo->commit();
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            throw $e;
        }
    }
}