<?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\Extbase\Core;

use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Mvc\Dispatcher;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder;
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
use TYPO3\CMS\Extbase\Service\CacheService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;

/**
 * Creates a request and dispatches it to the controller which was specified
 * by TS Setup and returns the content.
 *
 * This class is the main entry point for extbase extensions.
 */
class Bootstrap
{
    public static array $persistenceClasses = [];

    /**
     * Set by UserContentObject (USER) via setContentObjectRenderer() in frontend
     */
    protected ?ContentObjectRenderer $cObj = null;

    protected ContainerInterface $container;
    protected ConfigurationManagerInterface $configurationManager;
    protected PersistenceManagerInterface $persistenceManager;
    protected CacheService $cacheService;
    protected Dispatcher $dispatcher;
    protected RequestBuilder $extbaseRequestBuilder;

    public function __construct(
        ContainerInterface $container,
        ConfigurationManagerInterface $configurationManager,
        PersistenceManagerInterface $persistenceManager,
        CacheService $cacheService,
        Dispatcher $dispatcher,
        RequestBuilder $extbaseRequestBuilder
    ) {
        $this->container = $container;
        $this->configurationManager = $configurationManager;
        $this->persistenceManager = $persistenceManager;
        $this->cacheService = $cacheService;
        $this->dispatcher = $dispatcher;
        $this->extbaseRequestBuilder = $extbaseRequestBuilder;
    }

    /**
     * Called for frontend plugins from UserContentObject via ContentObjectRenderer->callUserFunction().
     */
    public function setContentObjectRenderer(ContentObjectRenderer $cObj)
    {
        $this->cObj = $cObj;
    }

    /**
     * Explicitly initializes all necessary Extbase objects by invoking the various initialize* methods.
     *
     * Usually this method is only called from unit tests or other applications which need a more fine grained control over
     * the initialization and request handling process. Most other applications just call the run() method.
     *
     * @param array $configuration The TS configuration array
     * @throws \RuntimeException
     * @see run()
     */
    public function initialize(array $configuration): void
    {
        if (!Environment::isCli()) {
            if (!isset($configuration['extensionName']) || $configuration['extensionName'] === '') {
                throw new \RuntimeException('Invalid configuration: "extensionName" is not set', 1290623020);
            }
            if (!isset($configuration['pluginName']) || $configuration['pluginName'] === '') {
                throw new \RuntimeException('Invalid configuration: "pluginName" is not set', 1290623027);
            }
        }
        $this->initializeConfiguration($configuration);
    }

    /**
     * Initializes the Object framework.
     *
     * @see initialize()
     * @internal
     */
    public function initializeConfiguration(array $configuration): void
    {
        $this->cObj ??= $this->container->get(ContentObjectRenderer::class);
        $this->configurationManager->setContentObject($this->cObj);
        $this->configurationManager->setConfiguration($configuration);
        // todo: Shouldn't the configuration manager object – which is a singleton – be stateless?
        // todo: At this point we give the configuration manager a state, while we could directly pass the
        // todo: configuration (i.e. controllerName, actionName and such), directly to the request
        // todo: handler, which then creates stateful request objects.
        // todo: Once this has changed, \TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder::loadDefaultValues does not need
        // todo: to fetch this configuration from the configuration manager.
    }

    /**
     * Runs the Extbase Framework by resolving an appropriate Request Handler and passing control to it.
     * If the Framework is not initialized yet, it will be initialized.
     *
     * This is usually used in Frontend plugins.
     *
     * @param string $content The content. Not used
     * @param array $configuration The TS configuration array
     * @param ServerRequestInterface $request the incoming server request
     * @return string $content The processed content
     */
    public function run(string $content, array $configuration, ServerRequestInterface $request): string
    {
        $this->initialize($configuration);
        return $this->handleFrontendRequest($request);
    }

    protected function handleFrontendRequest(ServerRequestInterface $request): string
    {
        $extbaseRequest = $this->extbaseRequestBuilder->build($request);
        if (!$this->isExtbaseRequestCacheable($extbaseRequest)) {
            if ($this->cObj->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER) {
                // ContentObjectRenderer::convertToUserIntObject() will recreate the object,
                // so we have to stop the request here before the action is actually called
                $this->cObj->convertToUserIntObject();
                return '';
            }
        }

        // Dispatch the extbase request
        $response = $this->dispatcher->dispatch($extbaseRequest);
        if ($response->getStatusCode() >= 300) {
            // Avoid caching the plugin when we issue a redirect or error response
            // This means that even when an action is configured as cachable
            // we avoid the plugin to be cached, but keep the page cache untouched
            if ($this->cObj->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER) {
                $this->cObj->convertToUserIntObject();
            }
        }
        // Usually coming from an error action, ensure all caches are cleared
        if ($response->getStatusCode() === 400) {
            $this->clearCacheOnError();
        }

        // In case TSFE is available and this is a json response, we have to let TSFE know we have a specific Content-Type
        if (($typoScriptFrontendController = ($GLOBALS['TSFE'] ?? null)) instanceof TypoScriptFrontendController
            && str_starts_with($response->getHeaderLine('Content-Type'), 'application/json')
        ) {
            // Do not send the header directly (see below)
            $response = $response->withoutHeader('Content-Type');
            $typoScriptFrontendController->setContentType('application/json');
        }

        if (headers_sent() === false) {
            foreach ($response->getHeaders() as $name => $values) {
                foreach ($values as $value) {
                    header(sprintf('%s: %s', $name, $value));
                }
            }

            // Set status code from extbase response
            // @todo: Remove when ContentObjectRenderer is response aware
            if ($response->getStatusCode() >= 300) {
                header('HTTP/' . $response->getProtocolVersion() . ' ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
            }
        }
        $body = $response->getBody();
        $body->rewind();
        $content = $body->getContents();
        $this->resetSingletons();
        $this->cacheService->clearCachesOfRegisteredPageIds();
        return $content;
    }

    /**
     * Entrypoint for backend modules, handling PSR-7 requests/responses.
     *
     * Creates an Extbase Request, dispatches it and then returns the Response
     *
     * @param ServerRequestInterface $request
     * @internal
     */
    public function handleBackendRequest(ServerRequestInterface $request): ResponseInterface
    {
        // build the configuration from the module, included in the current request
        $module = $request->getAttribute('module');
        $configuration = [
            'extensionName' => $module?->getExtensionName(),
            'pluginName' => $module?->getIdentifier(),
        ];

        $this->initialize($configuration);
        $extbaseRequest = $this->extbaseRequestBuilder->build($request);
        $response = $this->dispatcher->dispatch($extbaseRequest);
        $this->resetSingletons();
        $this->cacheService->clearCachesOfRegisteredPageIds();
        return $response;
    }

    /**
     * Clear cache of current page on error. Needed because we want a re-evaluation of the data.
     */
    protected function clearCacheOnError(): void
    {
        $extbaseSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
        if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') {
            if (isset($GLOBALS['TSFE'])) {
                $this->cacheService->clearPageCache([$GLOBALS['TSFE']->id]);
            }
        }
    }

    /**
     * Resets global singletons for the next plugin
     */
    protected function resetSingletons(): void
    {
        $this->persistenceManager->persistAll();
    }

    protected function isExtbaseRequestCacheable(RequestInterface $extbaseRequest): bool
    {
        $controllerClassName = $extbaseRequest->getControllerObjectName();
        $actionName = $extbaseRequest->getControllerActionName();
        $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
        $nonCacheableActions = $frameworkConfiguration['controllerConfiguration'][$controllerClassName]['nonCacheableActions'] ?? null;
        if (!is_array($nonCacheableActions)) {
            return true;
        }
        return !in_array($actionName, $nonCacheableActions, true);
    }
}
