<?php

declare(strict_types=1);

namespace Tournament\Bracket;

use PDO;
use RuntimeException;
use Throwable;

final class MatchResultService
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly ResultCalculator $resultCalculator,
        private readonly BracketProgression $progression
    ) {
    }

    /**
     * Submit and finalize a match result.
     *
     * @param array<int, array{
     *     participant_id: int,
     *     placement?: int|null,
     *     kills?: int|null,
     *     score?: float|int|null
     * }> $results
     *
     * @return array{
     *     match_id: int,
     *     winner_participant_id: int,
     *     loser_participant_ids: array<int, int>,
     *     scores: array<int, float>,
     *     next_match_id: int|null,
     *     tournament_completed: bool
     * }
     */
    public function submit(
        int $matchId,
        array $results,
        int $submittedBy
    ): array {
        if ($matchId <= 0) {
            throw new RuntimeException(
                'Invalid match ID.'
            );
        }

        if ($submittedBy <= 0) {
            throw new RuntimeException(
                'Invalid submitter.'
            );
        }

        $this->pdo->beginTransaction();

        try {
            $match = $this->getMatchForUpdate(
                $matchId
            );

            /*
             * Prevent duplicate submission.
             */
            if ($match['status'] === 'finished') {
                throw new RuntimeException(
                    'Match has already been finished.'
                );
            }

            if (
                !in_array(
                    $match['status'],
                    ['ready', 'live'],
                    true
                )
            ) {
                throw new RuntimeException(
                    'Match cannot accept results in its current status.'
                );
            }

            $this->validateParticipants(
                $matchId,
                $results
            );

            /*
             * Calculate final result.
             */
            $calculated =
                $this->resultCalculator->calculate(
                    $results
                );

            /*
             * Save results.
             */
            $this->saveResults(
                $matchId,
                $results,
                $calculated,
                $submittedBy
            );

            /*
             * Finish match.
             */
            $this->finishMatch(
                $matchId,
                $calculated['winner_participant_id']
            );

            /*
             * Update tournament participant statuses.
             */
            $this->updateTournamentParticipantStatuses(
                $calculated['winner_participant_id'],
                $calculated['loser_participant_ids']
            );

            /*
             * Update participants inside this match.
             */
            $this->updateMatchParticipantStatuses(
                $matchId,
                $calculated['winner_participant_id'],
                $calculated['loser_participant_ids']
            );

            /*
             * Advance winner.
             */
            $progression =
                $this->progression->advanceWinner(
                    $matchId,
                    $calculated['winner_participant_id']
                );

            $this->pdo->commit();

            return [
                'match_id' =>
                    $matchId,

                'winner_participant_id' =>
                    $calculated['winner_participant_id'],

                'loser_participant_ids' =>
                    $calculated['loser_participant_ids'],

                'scores' =>
                    $calculated['scores'],

                'next_match_id' =>
                    $progression['next_match_id'],

                'tournament_completed' =>
                    $progression['tournament_completed'],
            ];
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            throw $e;
        }
    }

    /**
     * Get and lock match.
     */
    private function getMatchForUpdate(
        int $matchId
    ): array {
        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                id,
                bracket_id,
                stage_id,
                status,
                winner_participant_id
            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 submitted participants.
     */
    private function validateParticipants(
        int $matchId,
        array $results
    ): void {
        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                tournament_participant_id
            FROM tournament_match_participants
            WHERE match_id = ?
            ORDER BY slot ASC
            FOR UPDATE
            SQL
        );

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

        $expected = array_map(
            'intval',
            $statement->fetchAll(
                PDO::FETCH_COLUMN
            )
        );

        if (count($expected) < 2) {
            throw new RuntimeException(
                'Match does not have enough participants.'
            );
        }

        $submitted = [];

        foreach ($results as $result) {
            if (!isset($result['participant_id'])) {
                throw new RuntimeException(
                    'Participant ID is required.'
                );
            }

            $submitted[] =
                (int) $result['participant_id'];
        }

        sort($expected);
        sort($submitted);

        if ($expected !== $submitted) {
            throw new RuntimeException(
                'Submitted participants do not match the match participants.'
            );
        }
    }

    /**
     * Save result rows.
     */
    private function saveResults(
        int $matchId,
        array $results,
        array $calculated,
        int $submittedBy
    ): void {
        /*
         * IMPORTANT:
         *
         * This implementation intentionally uses
         * UPDATE/INSERT separately instead of relying
         * on a UNIQUE constraint that may not exist
         * in the current database.
         */
        foreach ($results as $result) {
            $participantId =
                (int) $result['participant_id'];

            $score =
                $calculated['scores'][$participantId];

            $placement =
                isset($result['placement'])
                    ? (int) $result['placement']
                    : null;

            $kills =
                isset($result['kills'])
                    ? (int) $result['kills']
                    : null;

            $isWinner =
                $participantId ===
                $calculated['winner_participant_id']
                    ? 1
                    : 0;

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

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

            $existingId =
                $statement->fetchColumn();

            if ($existingId !== false) {
                throw new RuntimeException(
                    'Result already exists for a participant in this match.'
                );
            }

            $statement = $this->pdo->prepare(
                <<<SQL
                INSERT INTO tournament_match_results (
                    match_id,
                    tournament_participant_id,
                    score,
                    placement,
                    kills,
                    is_winner,
                    submitted_by,
                    submitted_at
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
                SQL
            );

            $statement->execute([
                $matchId,
                $participantId,
                $score,
                $placement,
                $kills,
                $isWinner,
                $submittedBy,
            ]);
        }
    }

    /**
     * Finish match.
     */
    private function finishMatch(
        int $matchId,
        int $winnerParticipantId
    ): void {
        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_matches
            SET
                status = 'finished',
                winner_participant_id = ?,
                finished_at = COALESCE(
                    finished_at,
                    NOW()
                )
            WHERE id = ?
              AND status IN ('ready', 'live')
            SQL
        );

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

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

    /**
     * Update global tournament participant state.
     */
    private function updateTournamentParticipantStatuses(
        int $winnerParticipantId,
        array $loserParticipantIds
    ): void {
        /*
         * Winner stays active unless this is
         * the final. BracketProgression will
         * convert it to winner.
         */
        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_participants
            SET status = 'active'
            WHERE id = ?
              AND status = 'active'
            SQL
        );

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

        /*
         * Eliminate losers globally.
         */
        if ($loserParticipantIds === []) {
            return;
        }

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

        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_participants
            SET status = 'eliminated'
            WHERE id IN ($placeholders)
              AND status = 'active'
            SQL
        );

        $statement->execute(
            $loserParticipantIds
        );
    }

    /**
     * Update participant states inside match.
     */
    private function updateMatchParticipantStatuses(
        int $matchId,
        int $winnerParticipantId,
        array $loserParticipantIds
    ): void {
        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_match_participants
            SET status = 'finished'
            WHERE match_id = ?
              AND tournament_participant_id = ?
            SQL
        );

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

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

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

        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_match_participants
            SET status = 'eliminated'
            WHERE match_id = ?
              AND tournament_participant_id IN ($placeholders)
            SQL
        );

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