<?php

declare(strict_types=1);

namespace Tournament\Result;

use Tournament\Scoring\ScoringStrategy;
use Tournament\Scoring\TieBreakerChain;
use RuntimeException;

final class ResultCalculator
{
    public function __construct(
        private readonly ScoringStrategy $scoringStrategy,
        private readonly TieBreakerChain $tieBreakerChain
    ) {
    }

    /**
     * Calculate match result.
     *
     * @param array<int, array{
     *     participant_id: int,
     *     score?: float|int|null,
     *     placement?: int|null,
     *     kills?: int|null
     * }> $results
     *
     * @return array{
     *     winner_participant_id: int,
     *     loser_participant_ids: array<int, int>,
     *     scores: array<int, float>
     * }
     */
    public function calculate(
        array $results
    ): array {
        $this->validate($results);

        $scores = [];

        foreach ($results as $result) {
            $participantId =
                (int) $result['participant_id'];

            $scores[$participantId] =
                $this->scoringStrategy->calculateScore(
                    $result
                );
        }

        /*
         * Prepare data for tie-breakers.
         */
        $scoredResults = [];

        foreach ($results as $result) {
            $participantId =
                (int) $result['participant_id'];

            $scoredResults[] = [
                'participant_id' =>
                    $participantId,

                'score' =>
                    $scores[$participantId],

                'placement' =>
                    $result['placement'] ?? null,

                'kills' =>
                    $result['kills'] ?? null,
            ];
        }

        $winnerId = $this->findWinner(
            $scoredResults,
            $scores
        );

        $loserIds = [];

        foreach ($scores as $participantId => $score) {
            if ($participantId !== $winnerId) {
                $loserIds[] = $participantId;
            }
        }

        return [
            'winner_participant_id' =>
                $winnerId,

            'loser_participant_ids' =>
                $loserIds,

            'scores' =>
                $scores,
        ];
    }

    /**
     * Validate raw result data.
     */
    private function validate(
        array $results
    ): void {
        if (count($results) < 2) {
            throw new RuntimeException(
                'At least two participants are required.'
            );
        }

        $participantIds = [];

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

            $participantId =
                (int) $result['participant_id'];

            if ($participantId <= 0) {
                throw new RuntimeException(
                    'Invalid participant ID.'
                );
            }

            if (
                isset($participantIds[$participantId])
            ) {
                throw new RuntimeException(
                    'Duplicate participant.'
                );
            }

            $participantIds[$participantId] = true;

            /*
             * Placement.
             */
            if (
                isset($result['placement'])
                && $result['placement'] !== null
                && (
                    !is_numeric($result['placement'])
                    || (int) $result['placement'] < 1
                )
            ) {
                throw new RuntimeException(
                    'Placement must be greater than zero.'
                );
            }

            /*
             * Kills.
             */
            if (
                isset($result['kills'])
                && $result['kills'] !== null
                && (
                    !is_numeric($result['kills'])
                    || (int) $result['kills'] < 0
                )
            ) {
                throw new RuntimeException(
                    'Kills cannot be negative.'
                );
            }
        }
    }

    /**
     * Find winner using score first,
     * then the configured tie-breaker chain.
     *
     * @param array<int, array{
     *     participant_id: int,
     *     score: float,
     *     placement?: int|null,
     *     kills?: int|null
     * }> $scoredResults
     *
     * @param array<int, float> $scores
     */
    private function findWinner(
        array $scoredResults,
        array $scores
    ): int {
        $highestScore =
            max($scores);

        /*
         * Find all participants with
         * the highest score.
         */
        $tiedResults = [];

        foreach ($scoredResults as $result) {
            if (
                $result['score'] === $highestScore
            ) {
                $tiedResults[] = $result;
            }
        }

        /*
         * Clear winner.
         */
        if (count($tiedResults) === 1) {
            return (int) $tiedResults[0]['participant_id'];
        }

        /*
         * Resolve tie.
         */
        $winnerId =
            $this->tieBreakerChain->resolve(
                $tiedResults
            );

        if ($winnerId === null) {
            throw new RuntimeException(
                'Match result has an unresolved tie.'
            );
        }

        return $winnerId;
    }
}