<?php declare(strict_types=1);

namespace App\Controller\API;

use App\DataTransferObject\Scoreboard\Problem;
use App\DataTransferObject\Scoreboard\Row;
use App\DataTransferObject\Scoreboard\Score;
use App\DataTransferObject\Scoreboard\Scoreboard;
use App\Entity\Contest;
use App\Entity\Event;
use App\Entity\ScoreboardType;
use App\Entity\TeamCategory;
use App\Service\AuthorizedUserService;
use App\Service\ConfigurationService;
use App\Service\DOMJudgeService;
use App\Service\EventLogService;
use App\Service\ScoreboardService;
use App\Utils\Scoreboard\Filter;
use App\Utils\Utils;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use FOS\RestBundle\Controller\Annotations as Rest;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

#[Rest\Route(path: '/contests/{cid}/scoreboard')]
#[OA\Tag(name: 'Scoreboard')]
#[OA\Parameter(ref: '#/components/parameters/cid')]
#[OA\Parameter(ref: '#/components/parameters/strict')]
#[OA\Response(ref: '#/components/responses/InvalidResponse', response: 400)]
#[OA\Response(ref: '#/components/responses/Unauthenticated', response: 401)]
#[OA\Response(ref: '#/components/responses/Unauthorized', response: 403)]
#[OA\Response(ref: '#/components/responses/NotFound', response: 404)]
class ScoreboardController extends AbstractApiController
{
    public function __construct(
        AuthorizedUserService $authService,
        EntityManagerInterface $em,
        DOMJudgeService $dj,
        ConfigurationService $config,
        EventLogService $eventLogService,
        protected readonly ScoreboardService $scoreboardService
    ) {
        parent::__construct($authService, $em, $dj, $config, $eventLogService);
    }

    /**
     * Get the scoreboard for this contest.
     * @throws NonUniqueResultException
     */
    #[Rest\Get(path: '')]
    #[OA\Response(
        response: 200,
        description: 'Returns the scoreboard',
        content: new OA\JsonContent(ref: new Model(type: Scoreboard::class))
    )]
    #[OA\Parameter(
        name: 'allteams',
        description: 'Also show invisible teams. Requires jury privileges',
        in: 'query',
        schema: new OA\Schema(type: 'boolean')
    )]
    #[OA\Parameter(
        name: 'category',
        description: 'Get the scoreboard for only this category',
        in: 'query',
        schema: new OA\Schema(type: 'string')
    )]
    #[OA\Parameter(
        name: 'country',
        description: 'Get the scoreboard for only this country (in ISO 3166-1 alpha-3 format)',
        in: 'query',
        schema: new OA\Schema(type: 'string')
    )]
    #[OA\Parameter(
        name: 'affiliation',
        description: 'Get the scoreboard for only this affiliation',
        in: 'query',
        schema: new OA\Schema(type: 'string')
    )]
    #[OA\Parameter(
        name: 'public',
        description: 'Show publicly visible scoreboard, even for users with more permissions',
        in: 'query',
        schema: new OA\Schema(type: 'boolean')
    )]
    #[OA\Parameter(
        name: 'sortorder',
        description: 'The sort order to get the scoreboard for. If not given, uses the lowest sortorder',
        in: 'query',
        schema: new OA\Schema(type: 'integer')
    )]
    public function getScoreboardAction(
        Request $request,
        #[MapQueryParameter]
        ?string $category = null,
        #[MapQueryParameter]
        ?string $country = null,
        #[MapQueryParameter]
        ?string $affiliation = null,
        #[MapQueryParameter(name: 'allteams')]
        bool $allTeams = false,
        #[MapQueryParameter(name: 'public')]
        ?bool $publicInRequest = null,
        #[MapQueryParameter]
        ?int $sortorder = null,
        #[MapQueryParameter]
        bool $strict = false,
    ): Scoreboard {
        if (!$this->config->get('enable_ranking') && !$this->authService->checkRole('jury')) {
            throw new BadRequestHttpException('Scoreboard is not available.');
        }

        $filter = new Filter();
        if ($category) {
            $filter->categories = [$category];
        }
        if ($country) {
            $filter->countries = [$country];
        }
        if ($affiliation) {
            $filter->affiliations = [$affiliation];
        }
        $public   = !$this->authService->checkRole('api_reader');
        if ($this->authService->checkRole('api_reader') && $publicInRequest !== null) {
            $public = $publicInRequest;
        }
        if ($sortorder === null) {
            // Get the lowest available sortorder.
            $queryBuilder = $this->em->createQueryBuilder()
                ->from(TeamCategory::class, 'c')
                ->select('MIN(c.sortorder)');
            if ($public) {
                $queryBuilder->andWhere('c.visible = 1');
            }
            $sortorder = (int)$queryBuilder->getQuery()->getSingleScalarResult();
        }

        /** @var Contest $contest */
        // Also checks access of user to the contest via getContestQueryBuilder() from superclass.
        $contest = $this->em->getRepository(Contest::class)->find($this->getContestId($request));

        // Get the event for this scoreboard.
        /** @var Event|null $event */
        $event = $this->em->createQueryBuilder()
            ->from(Event::class, 'e')
            ->select('e')
            ->orderBy('e.eventid', 'DESC')
            ->setMaxResults(1)
            ->getQuery()
            ->getOneOrNullResult();

        $scoreboard = $this->scoreboardService->getScoreboard($contest, !$public, $filter, !$allTeams);

        $results = new Scoreboard();
        if ($event) {
            // Build up scoreboard results.
            $results = new Scoreboard(
                eventId: (string)$event->getEventid(),
                time: Utils::absTime($event->getEventtime()),
                contestTime: Utils::relTime($event->getEventtime() - $contest->getStarttime()),
                state: $contest->getState(),
            );
        }

        // Return early if there's nothing to display yet.
        if (!$scoreboard) {
            return $results;
        }

        $scoreIsInSeconds = (bool)$this->config->get('score_in_seconds');

        foreach ($scoreboard->getScores() as $teamScore) {
            if ($teamScore->team->getSortorder() !== $sortorder) {
                continue;
            }

            $isScoring = $contest->getScoreboardType() === ScoreboardType::SCORE;
            $totalScore = $isScoring ? (float)$teamScore->score : null;

            if ($contest->getRuntimeAsScoreTiebreaker()) {
                $score = new Score(
                    numSolved: $teamScore->numPoints,
                    totalRuntime: $teamScore->totalRuntime,
                    totalScore: $totalScore,
                );
            } else {
                $score = new Score(
                    numSolved: $teamScore->numPoints,
                    totalTime: $isScoring ? null : $teamScore->totalTime,
                    totalScore: $totalScore,
                );
            }

            $problems = [];
            foreach ($scoreboard->getMatrix()[$teamScore->team->getTeamid()] as $problemId => $matrixItem) {
                $contestProblem = $scoreboard->getProblems()[$problemId];
                $problem = new Problem(
                    label: $contestProblem->getShortname(),
                    problemId: $contestProblem->getProblem()->getExternalid(),
                    numJudged: $matrixItem->numSubmissions,
                    numPending: $matrixItem->numSubmissionsPending,
                    solved: $matrixItem->isCorrect,
                );

                if ($contest->getRuntimeAsScoreTiebreaker()) {
                    $problem->fastestSubmission = $matrixItem->isCorrect && $scoreboard->isFastestSubmission($teamScore->team, $contestProblem);
                    if ($matrixItem->isCorrect) {
                        $problem->runtime = $matrixItem->runtime;
                    }
                } else {
                    $problem->firstToSolve = $matrixItem->isCorrect && $scoreboard->solvedFirst($teamScore->team, $contestProblem);
                    if ($matrixItem->isCorrect) {
                        $problem->time = Utils::scoretime($matrixItem->time, $scoreIsInSeconds);
                    }
                }

                if ($contestProblem->getProblem()->isScoringProblem() && $matrixItem->points !== "") {
                    $problem->score = (float)$matrixItem->points;
                }

                $problems[] = $problem;
            }

            usort($problems, fn(Problem $a, Problem $b) => $a->label <=> $b->label);

            $row = new Row(
                rank: $teamScore->rank,
                teamId: $teamScore->team->getExternalid(),
                score: $score,
                problems: $problems,
            );

            $results->rows[] = $row;
        }

        return $results;
    }
}
