<?php

declare(strict_types=1);

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao\CoreBundle\Routing\ResponseContext;

use Contao\CoreBundle\Controller\CspReporterController;
use Contao\CoreBundle\InsertTag\InsertTagParser;
use Contao\CoreBundle\Routing\ResponseContext\Csp\CspHandlerFactory;
use Contao\CoreBundle\Routing\ResponseContext\HtmlHeadBag\HtmlHeadBag;
use Contao\CoreBundle\Routing\ResponseContext\JsonLd\ContaoPageSchema;
use Contao\CoreBundle\Routing\ResponseContext\JsonLd\JsonLdManager;
use Contao\CoreBundle\Security\Authentication\Token\TokenChecker;
use Contao\CoreBundle\String\HtmlDecoder;
use Contao\CoreBundle\Util\UrlUtil;
use Contao\FrontendUser;
use Contao\PageModel;
use Contao\StringUtil;
use Spatie\SchemaOrg\WebPage;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class CoreResponseContextFactory
{
    public function __construct(
        private readonly ResponseContextAccessor $responseContextAccessor,
        private readonly EventDispatcherInterface $eventDispatcher,
        private readonly TokenChecker $tokenChecker,
        private readonly HtmlDecoder $htmlDecoder,
        private readonly RequestStack $requestStack,
        private readonly InsertTagParser $insertTagParser,
        private readonly CspHandlerFactory $cspHandlerFactory,
        private readonly UrlGeneratorInterface $urlGenerator,
        private readonly Security $security,
    ) {
    }

    public function createResponseContext(): ResponseContext
    {
        $context = new ResponseContext();

        $this->responseContextAccessor->setResponseContext($context);

        return $context;
    }

    public function createWebpageResponseContext(): ResponseContext
    {
        $context = $this->createResponseContext();
        $context->add($this->eventDispatcher);
        $context->addLazy(HtmlHeadBag::class);

        $context->addLazy(
            JsonLdManager::class,
            static function () use ($context) {
                $manager = new JsonLdManager($context);
                $manager->getGraphForSchema(JsonLdManager::SCHEMA_ORG)->add(new WebPage());

                return $manager;
            },
        );

        return $context;
    }

    public function createContaoWebpageResponseContext(PageModel $pageModel): ResponseContext
    {
        $context = $this->createWebpageResponseContext();
        $title = $this->htmlDecoder->inputEncodedToPlainText(($pageModel->pageTitle ?: $pageModel->title) ?: '');

        $htmlHeadBag = $context->get(HtmlHeadBag::class);
        $htmlHeadBag
            ->setTitle($title ?: '')
            ->setMetaDescription($this->htmlDecoder->inputEncodedToPlainText($pageModel->description ?: ''))
        ;

        if ($pageModel->robots) {
            $htmlHeadBag->setMetaRobots($pageModel->robots);
        }

        if ($pageModel->enableCanonical && $pageModel->canonicalLink) {
            $url = $this->insertTagParser->replaceInline($pageModel->canonicalLink);

            // Ensure absolute links
            if (!preg_match('#^https?://#', $url)) {
                if (!$request = $this->requestStack->getCurrentRequest()) {
                    throw new \RuntimeException('The request stack did not contain a request');
                }

                $url = UrlUtil::makeAbsolute($url, $request->getSchemeAndHttpHost().$request->getBasePath().'/');
            }

            $htmlHeadBag->setCanonicalUri($url);
        }

        if ($pageModel->enableCanonical && $pageModel->canonicalKeepParams) {
            $htmlHeadBag->setKeepParamsForCanonical(array_map(trim(...), explode(',', $pageModel->canonicalKeepParams)));
        }

        $jsonLdManager = $context->get(JsonLdManager::class);
        $jsonLdManager
            ->getGraphForSchema(JsonLdManager::SCHEMA_CONTAO)
            ->set(
                new ContaoPageSchema(
                    $title ?: '',
                    $pageModel->id,
                    $pageModel->noSearch ?? false,
                    $pageModel->protected,
                    array_map(\intval(...), array_filter((array) $pageModel->groups)),
                    $this->tokenChecker->isPreviewMode(),
                    $this->getMemberGroups(),
                    $pageModel->searchIndexer,
                ),
            )
        ;

        $this->addCspHandler($context, $pageModel);

        return $context;
    }

    private function addCspHandler(ResponseContext $context, PageModel $pageModel): void
    {
        if (!$pageModel->enableCsp) {
            return;
        }

        $cspHandler = $this->cspHandlerFactory->create((string) $pageModel->csp);
        $cspHandler->getDirectives()->setLevel1Fallback(false);
        $cspHandler->setReportOnly($pageModel->cspReportOnly);

        if ($pageModel->cspReportLog) {
            $urlContext = $this->urlGenerator->getContext();
            $baseUrl = $urlContext->getBaseUrl();

            // Remove preview script if present
            $urlContext->setBaseUrl('');

            try {
                $reportUri = $this->urlGenerator->generate(CspReporterController::class, ['page' => $pageModel->id], UrlGeneratorInterface::ABSOLUTE_URL);
                $cspHandler->getDirectives()->setDirective('report-uri', $reportUri);
            } catch (RouteNotFoundException) {
                // noop
            } finally {
                $urlContext->setBaseUrl($baseUrl);
            }
        }

        $context->add($cspHandler);
    }

    /**
     * @return array<int>
     */
    private function getMemberGroups(): array
    {
        $user = $this->security->getUser();

        if (!$user instanceof FrontendUser) {
            return [];
        }

        return array_map(\intval(...), array_filter((array) StringUtil::deserialize($user->groups, true)));
    }
}
