<?php

declare(strict_types=1);

namespace Tournament\Bracket;

use PDO;
use RuntimeException;

final class MatchStateManager
{
    private const STATUS_PENDING   = 'pending';
    private const STATUS_READY     = 'ready';
    private const STATUS_LIVE      = 'live';
    private const STATUS_FINISHED  = 'finished';
    private const STATUS_DISPUTED  = 'disputed';
    private const STATUS_CANCELLED = 'cancelled';

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

    /**
     * Mark a pending match as ready.
     */
    public function ready(int $matchId): void
    {
        $this->changeStatus(
            $matchId,
            self::STATUS_PENDING,
            self::STATUS_READY
        );
    }

    /**
     * Start a ready match.
     */
    public function start(int $matchId): void
    {
        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_matches
            SET
                status = ?,
                started_at = NOW()
            WHERE id = ?
              AND status = ?
            SQL
        );

        $statement->execute([
            self::STATUS_LIVE,
            $matchId,
            self::STATUS_READY,
        ]);

        if ($statement->rowCount() !== 1) {
            throw new RuntimeException(
                'Match cannot be started.'
            );
        }
    }

    /**
     * Mark a live match as finished.
     *
     * Winner is stored separately by MatchResultService.
     */
    public function finish(int $matchId): void
    {
        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_matches
            SET
                status = ?,
                finished_at = NOW()
            WHERE id = ?
              AND status = ?
            SQL
        );

        $statement->execute([
            self::STATUS_FINISHED,
            $matchId,
            self::STATUS_LIVE,
        ]);

        if ($statement->rowCount() !== 1) {
            throw new RuntimeException(
                'Match cannot be finished.'
            );
        }
    }

    /**
     * Mark a match as disputed.
     */
    public function dispute(int $matchId): void
    {
        $this->changeStatus(
            $matchId,
            self::STATUS_FINISHED,
            self::STATUS_DISPUTED
        );
    }

    /**
     * Resolve a disputed match.
     */
    public function resolveDispute(int $matchId): void
    {
        $this->changeStatus(
            $matchId,
            self::STATUS_DISPUTED,
            self::STATUS_FINISHED
        );
    }

    /**
     * Cancel a match.
     *
     * Finished matches cannot be cancelled.
     */
    public function cancel(int $matchId): void
    {
        $match = $this->getMatch(
            $matchId
        );

        if ($match['status'] === self::STATUS_FINISHED) {
            throw new RuntimeException(
                'Finished match cannot be cancelled.'
            );
        }

        if ($match['status'] === self::STATUS_CANCELLED) {
            return;
        }

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

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

    /**
     * Get current match status.
     */
    public function getStatus(
        int $matchId
    ): string {
        $match = $this->getMatch(
            $matchId
        );

        return $match['status'];
    }

    /**
     * Check whether match is ready.
     */
    public function isReady(
        int $matchId
    ): bool {
        return $this->getStatus($matchId)
            === self::STATUS_READY;
    }

    /**
     * Check whether match is live.
     */
    public function isLive(
        int $matchId
    ): bool {
        return $this->getStatus($matchId)
            === self::STATUS_LIVE;
    }

    /**
     * Check whether match is finished.
     */
    public function isFinished(
        int $matchId
    ): bool {
        return $this->getStatus($matchId)
            === self::STATUS_FINISHED;
    }

    /**
     * Change status with strict transition.
     */
    private function changeStatus(
        int $matchId,
        string $from,
        string $to
    ): void {
        $statement = $this->pdo->prepare(
            <<<SQL
            UPDATE tournament_matches
            SET status = ?
            WHERE id = ?
              AND status = ?
            SQL
        );

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

        if ($statement->rowCount() !== 1) {
            throw new RuntimeException(
                sprintf(
                    'Cannot change match %d status from "%s" to "%s".',
                    $matchId,
                    $from,
                    $to
                )
            );
        }
    }

    /**
     * Get match.
     */
    private function getMatch(
        int $matchId
    ): array {
        $statement = $this->pdo->prepare(
            <<<SQL
            SELECT
                id,
                bracket_id,
                stage_id,
                round_number,
                match_number,
                status,
                is_bye,
                winner_participant_id,
                scheduled_at,
                started_at,
                finished_at
            FROM tournament_matches
            WHERE id = ?
            LIMIT 1
            SQL
        );

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

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

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

        return $match;
    }
}