<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Backend\Security\ContentSecurityPolicy;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Uid\UuidV4;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\NullResponse;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\InvestigateMutationsEvent;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ModelService;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationSuggestion;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Report;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportAttribute;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportDemand;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportRepository;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportStatus;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Resolution;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ResolutionRepository;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\SummarizedReport;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;

/**
 * AJAX endpoint for the CSP backend module, providing access to persisted CSP reports & resolutions.
 * @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
 */
#[AsController]
readonly class CspAjaxController
{
    public function __construct(
        protected ModelService $modelService,
        protected PolicyProvider $policyProvider,
        protected ReportRepository $reportRepository,
        protected ResolutionRepository $resolutionRepository,
        protected EventDispatcherInterface $eventDispatcher,
        protected HashService $hashService,
    ) {}

    public function handleRequest(ServerRequestInterface $request): ResponseInterface
    {
        if ($request->getMethod() === 'GET') {
            return (new NullResponse())->withStatus(400);
        }
        if (!$this->isSystemMaintainer()) {
            return (new NullResponse())->withStatus(403);
        }
        return $this->dispatchAction($request)
            ?? (new NullResponse())->withStatus(500);
    }

    protected function dispatchAction(ServerRequestInterface $request): ?ResponseInterface
    {
        $parsedBody = $request->getParsedBody();
        $hmac = $parsedBody['hmac'] ?? null;
        $action = $parsedBody['action'] ?? null;
        $summaries = $parsedBody['summaries'] ?? [];
        $scope = Scope::tryFrom($parsedBody['scope'] ?? '');
        $uuid = $parsedBody['uuid'] ?? null;
        if ($uuid !== null) {
            $uuid = Uuidv4::fromString($uuid);
        }
        if (!empty($parsedBody['suggestion'])) {
            $suggestion = $this->modelService->buildMutationSuggestionFromArray($parsedBody['suggestion']);
        }
        // reports
        if ($action === 'fetchReports') {
            return $this->fetchReportsAction($scope);
        }
        if ($action === 'muteReport' && is_array($summaries)) {
            return $this->muteReportAction(...$summaries);
        }
        if ($action === 'deleteReport' && is_array($summaries)) {
            return $this->deleteReportAction(...$summaries);
        }
        if ($action === 'deleteReports') {
            return $this->deleteReportsAction($scope);
        }
        if ($action === 'handleReport' && $uuid !== null) {
            return $this->handleReportAction($uuid);
        }
        if ($action === 'mutateReport'
            && $scope !== null
            && is_array($summaries)
            && isset($suggestion)
            && hash_equals($suggestion->hmac(), $hmac)
        ) {
            return $this->mutateReportAction($scope, $suggestion, ...$summaries);
        }
        return null;
    }

    protected function fetchReportsAction(?Scope $scope): ResponseInterface
    {
        $demand = ReportDemand::create();
        $demand->scope = $scope;
        $reports = $this->reportRepository->findAllSummarized($demand);
        // @todo not sure whether this is a good idea performance-wise
        $reports = array_map(
            function (SummarizedReport $report): SummarizedReport {
                $event = $this->dispatchInvestigateMutationsEvent($report);
                if ($event->getMutationSuggestions() !== []) {
                    $mutationHashes = array_map(
                        static fn(MutationSuggestion $suggestion): string => $suggestion->hash(),
                        $event->getMutationSuggestions()
                    );
                    $report = $report->withMutationHashes(...$mutationHashes)
                        ->withAttribute(ReportAttribute::fixable);
                }
                return $report;
            },
            $reports
        );
        return new JsonResponse($reports);
    }

    protected function muteReportAction(string ...$summaries): ResponseInterface
    {
        $reports = $this->reportRepository->findBySummary(...$summaries);
        $uuids = array_map(static fn(Report $report): UuidV4 => $report->uuid, $reports);
        $this->reportRepository->updateStatus(ReportStatus::Muted, ...$uuids);
        return new JsonResponse(['uuids' => $uuids]);
    }

    protected function deleteReportAction(string ...$summaries): ResponseInterface
    {
        $reports = $this->reportRepository->findBySummary(...$summaries);
        $reportUuids = $this->resolveReportUuids(...$reports);
        $this->reportRepository->updateStatus(ReportStatus::Deleted, ...$reportUuids);
        return new JsonResponse(['uuids' => $reportUuids]);
    }

    protected function deleteReportsAction(?Scope $scope): ResponseInterface
    {
        $amount = $this->reportRepository->removeAll($scope);
        return new JsonResponse(['amount' => $amount]);
    }

    protected function handleReportAction(UuidV4 $uuid): ResponseInterface
    {
        $report = $this->reportRepository->findByUuid($uuid);
        if ($report === null) {
            return new JsonResponse();
        }
        $event = $this->dispatchInvestigateMutationsEvent($report);
        $suggestions = $event->getMutationSuggestions();
        // reverse sort by priority (higher priorities take precedence)
        usort($suggestions, static fn(MutationSuggestion $a, MutationSuggestion $b) => $b->priority <=> $a->priority);
        return new JsonResponse($suggestions);
    }

    protected function mutateReportAction(Scope $scope, MutationSuggestion $suggestion, string ...$initiators): ResponseInterface
    {
        $summary = $this->generateResolutionSummary($scope, $suggestion);
        $resolution = $this->resolutionRepository->findBySummary($summary);
        $reports = $this->reportRepository->findBySummary(...$initiators);
        if ($resolution !== null || $reports === []) {
            return new JsonResponse();
        }
        $resolution = new Resolution($summary, $scope, $suggestion->identifier, $suggestion->collection, ['initiators' => $initiators]);
        $this->resolutionRepository->add($resolution);
        $reportUuids = $this->resolveReportUuids(...$reports);
        $this->reportRepository->updateStatus(ReportStatus::Handled, ...$reportUuids);
        return new JsonResponse(['initiators' => $initiators, 'uuids' => $reportUuids]);
    }

    protected function dispatchInvestigateMutationsEvent(Report $report): InvestigateMutationsEvent
    {
        // @todo for future versions, it might be considered to distinguish `enforce` and `report` in the database
        $policy = $this->policyProvider->provideFor($report->scope, $report->details->resolveDisposition());
        $event = new InvestigateMutationsEvent($policy, $report);
        $this->eventDispatcher->dispatch($event);
        return $event;
    }

    protected function generateResolutionSummary(Scope $scope, MutationSuggestion $suggestion): string
    {
        return $this->hashService->hmac(
            json_encode([
                $scope,
                $suggestion->identifier,
                $suggestion->collection,
            ]),
            self::class,
        );
    }

    protected function resolveReportUuids(Report ...$reports): array
    {
        return array_map(static fn(Report $report): UuidV4 => $report->uuid, $reports);
    }

    protected function isSystemMaintainer(): bool
    {
        $backendUser = $GLOBALS['BE_USER'] ?? null;
        return $backendUser instanceof BackendUserAuthentication && $backendUser->isSystemMaintainer();
    }
}
