<?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\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration;
use TYPO3\CMS\Backend\Dto\Settings\EditableSetting;
use TYPO3\CMS\Backend\Exception\SiteValidationErrorException;
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup;
use TYPO3\CMS\Backend\Form\FormResultFactory;
use TYPO3\CMS\Backend\Form\FormResultHandler;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\Enum\ModuleLayout;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\SetupModuleViewMode;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException;
use TYPO3\CMS\Core\Configuration\Processor\Placeholder\EnvPlaceholderProcessor;
use TYPO3\CMS\Core\Configuration\SiteConfiguration;
use TYPO3\CMS\Core\Configuration\SiteWriter;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Schema\TcaSchemaBuilder;
use TYPO3\CMS\Core\Settings\Category;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsTypeRegistry;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Set\CategoryRegistry;
use TYPO3\CMS\Core\Site\Set\SetRegistry;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Site\SiteSettingsService;
use TYPO3\CMS\Core\SysLog\Action\Site as SiteAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\SysLog\Type;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;

/**
 * Setup module main controller implementing the two main views 'overview' with 'list'
 * and 'tiles', a details view, the edit view and save action.
 *
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
 */
#[AsController]
readonly class SiteConfigurationController
{
    public function __construct(
        protected ComponentFactory $componentFactory,
        protected SiteFinder $siteFinder,
        protected IconFactory $iconFactory,
        protected UriBuilder $uriBuilder,
        protected ModuleTemplateFactory $moduleTemplateFactory,
        private FormDataCompiler $formDataCompiler,
        private FormResultFactory $formResultFactory,
        private FormResultHandler $formResultHandler,
        private SiteConfiguration $siteConfiguration,
        private SiteWriter $siteWriter,
        private NodeFactory $nodeFactory,
        private SetRegistry $setRegistry,
        private CategoryRegistry $categoryRegistry,
        private SettingsTypeRegistry $settingsTypeRegistry,
        private SiteSettingsService $siteSettingsService,
        private FlashMessageService $flashMessageService,
        private ConnectionPool $connectionPool,
        private TcaSchemaBuilder $tcaSchemaBuilder,
        private EnvPlaceholderProcessor $envPlaceholderProcessor,
        private SiteTcaConfiguration $siteTcaConfiguration,
    ) {}

    /**
     * List pages that have 'is_siteroot' flag set - those that have the globe icon in page tree.
     * Link to Add / Edit / Delete for each.
     */
    public function overviewAction(ServerRequestInterface $request): ResponseInterface
    {
        $moduleData = $request->getAttribute('moduleData');
        $viewMode = SetupModuleViewMode::tryFrom($moduleData->get('viewMode') ?? '') ?? SetupModuleViewMode::TILES;
        $moduleData->set('viewMode', $viewMode->value);

        // forcing uncached sites will re-initialize `SiteFinder`
        // which is used later by FormEngine (implicit behavior)
        $allSites = $this->siteFinder->getAllSites(false);
        $pages = $this->getAllSitePages();
        $unassignedSites = [];
        $duplicatedRootPages = [];
        foreach ($allSites as $identifier => $site) {
            $rootPageId = $site->getRootPageId();
            if (isset($pages[$rootPageId]['siteConfiguration'])) {
                // rootPage is already used in a site configuration
                $duplicatedRootPages[$rootPageId][] = $pages[$rootPageId]['siteConfiguration']->getIdentifier();
                $duplicatedRootPages[$rootPageId][] = $site->getIdentifier();
                $duplicatedRootPages[$rootPageId] = array_unique($duplicatedRootPages[$rootPageId]);
            }
            if (isset($pages[$rootPageId])) {
                $pages[$rootPageId]['siteIdentifier'] = $identifier;
                $pages[$rootPageId]['siteConfiguration'] = $site;
            } else {
                $unassignedSites[] = $site;
            }
        }

        $rootPagesWithSiteConfiguration = [];
        $rootPagesWithoutSiteConfiguration = [];
        foreach ($pages as $page) {
            if (!isset($page['siteConfiguration'])) {
                $rootPagesWithoutSiteConfiguration[] = $page;
            } else {
                $rootPagesWithSiteConfiguration[] = $page;
            }
        }

        $view = $this->moduleTemplateFactory->create($request);
        $view->getDocHeaderComponent()->setShortcutContext(
            'site_configuration',
            $this->getLanguageService()->translate('short_description', 'backend.modules.site_configuration')
        );
        $this->addDocHeaderViewModeButton($view, $viewMode);
        $view->setTitle($this->getLanguageService()->translate('title', 'backend.modules.site_configuration'));
        $view->setLayout(ModuleLayout::NORMAL);
        $view->assignMultiple([
            'pages' => $pages,
            'viewMode' => $viewMode,
            'unassignedSites' => $unassignedSites,
            'duplicatedRootPages' => $duplicatedRootPages,
            'duplicatedEntryPoints' => $this->getDuplicatedEntryPoints($allSites, $pages),
            'invalidSets' => $this->setRegistry->getInvalidSets(),
            'rootPagesWithSiteConfiguration' => $rootPagesWithSiteConfiguration,
            'rootPagesWithoutSiteConfiguration' => $rootPagesWithoutSiteConfiguration,
        ]);

        return $view->renderResponse('SiteConfiguration/Overview');
    }

    /**
     * This lists all information about a site:
     * - URLs
     * - Languages + Translation Strategy
     */
    public function detailAction(ServerRequestInterface $request): ResponseInterface
    {
        $siteIdentifier = $request->getQueryParams()['site'] ?? null;
        if (empty($siteIdentifier)) {
            throw new \RuntimeException('Site identifier to show details must be set', 1763919655);
        }
        $site = $this->siteFinder->getSiteByIdentifier($siteIdentifier);
        $pageRecord = BackendUtility::getRecord('pages', $site->getRootPageId()) ?? [];

        $settings = $this->siteSettingsService->getUncachedSettings($site);
        $setSettings = $this->siteSettingsService->getSetSettings($site);

        $categoryEnhancer = function (Category $category) use (&$categoryEnhancer, $settings, $setSettings): Category {
            return new Category(...[
                ...get_object_vars($category),
                'label' => $this->getLanguageService()->sL($category->label),
                'description' => $category->description !== null ? $this->getLanguageService()->sL($category->description) : $category->description,
                'categories' => array_map($categoryEnhancer, $category->categories),
                'settings' => array_map(
                    fn(SettingDefinition $definition): EditableSetting => new EditableSetting(
                        definition: $this->resolveSettingLabels($definition),
                        value: $settings->get($definition->key),
                        systemDefault: $setSettings->get($definition->key),
                        typeImplementation: $this->settingsTypeRegistry->get($definition->type)->getJavaScriptModule(),
                    ),
                    $category->settings
                ),
            ]);
        };

        $categories = array_map($categoryEnhancer, $this->categoryRegistry->getCategories(...$site->getSets()));

        $view = $this->moduleTemplateFactory->create($request);
        $this->configureDetailViewDocHeader($view, $siteIdentifier, $request);
        $view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord);
        $view->setTitle(
            $this->getLanguageService()->translate('title', 'backend.modules.site_settings')
        );
        $view->setLayout(ModuleLayout::NORMAL);
        $view->assignMultiple([
            'site' => $site,
            'page' => $pageRecord,
            'categories' => $categories,
            'localSettings' => $this->siteSettingsService->getLocalSettings($site),
        ]);
        // @todo: Find CSP information (if active etc)
        return $view->renderResponse('SiteConfiguration/Detail');
    }

    /**
     * Shows a form to create a new site configuration, or edit an existing one.
     *
     * @throws \RuntimeException
     */
    public function editAction(ServerRequestInterface $request): ResponseInterface
    {
        // forcing uncached sites will re-initialize `SiteFinder`
        // which is used later by FormEngine (implicit behavior)
        $allSites = $this->siteFinder->getAllSites(false);

        $fullTca = array_merge($GLOBALS['TCA'], $this->siteTcaConfiguration->getTca());
        $pageUid = (int)($request->getQueryParams()['pageUid'] ?? 0);
        $siteIdentifier = $request->getQueryParams()['site'] ?? null;

        if (empty($siteIdentifier) && empty($pageUid)) {
            throw new \RuntimeException('Either site identifier to edit a config or page uid to add new config must be set', 1521561148);
        }
        $isNewConfig = empty($siteIdentifier);

        $defaultValues = [];
        if ($isNewConfig) {
            $defaultValues['site']['rootPageId'] = $pageUid;
            $pageRecord = BackendUtility::getRecord('pages', $pageUid) ?? [];
        } else {
            $site = $this->siteFinder->getSiteByIdentifier($siteIdentifier);
            $pageRecord = BackendUtility::getRecord('pages', $site->getRootPageId()) ?? [];
        }

        if (!$isNewConfig && !isset($allSites[$siteIdentifier])) {
            throw new \RuntimeException('Existing config for site ' . $siteIdentifier . ' not found', 1521561226);
        }

        $returnUrl = $this->resolveReturnUrl($request);

        $formDataCompilerInput = [
            'request' => $request,
            'tableName' => 'site',
            'vanillaUid' => $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId(),
            'command' => $isNewConfig ? 'new' : 'edit',
            'returnUrl' => $returnUrl,
            'customData' => [
                'siteIdentifier' => $isNewConfig ? '' : $siteIdentifier,
            ],
            'defaultValues' => $defaultValues,
            'tcaSchemata' => $this->tcaSchemaBuilder->buildFromStructure($fullTca),
            'fullTca' => $fullTca,
        ];
        $formData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(SiteConfigurationDataGroup::class));
        $formData['renderType'] = 'formWrapContainer';
        $formResult = $this->nodeFactory->create($formData)->render();
        $languageService = $this->getLanguageService();
        $documentTitle = $this->resolveDocumentTitle($languageService, $isNewConfig, $siteIdentifier);
        $formResult['html'] = '<h1>' . htmlspecialchars($documentTitle) . '</h1>' . $formResult['html'];
        $formResult = $this->formResultFactory->create($formResult);
        $this->formResultHandler->addAssets($formResult);

        $view = $this->moduleTemplateFactory->create($request);
        $view->assignMultiple([
            // Always add rootPageId as additional field to have a reference for new records
            'rootPageId' => $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId(),
            'returnUrl' => $returnUrl,
            'formEngineHtml' => $formResult->html,
        ]);
        $this->configureEditViewDocHeader($view, $siteIdentifier, $documentTitle);
        $view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord);
        $view->setTitle($documentTitle);
        $view->setLayout(ModuleLayout::NORMAL);
        return $view->renderResponse('SiteConfiguration/Edit');
    }

    /**
     * Save incoming data from editAction and redirect to overview or edit
     *
     * @throws \RuntimeException
     */
    public function saveAction(ServerRequestInterface $request): ResponseInterface
    {
        // loading uncached site configurations without settings.yaml
        /** @var array<int, Site> $mappingRootPageToSite */
        $mappingRootPageToSite = [];
        $allSites = $this->siteConfiguration->resolveAllExistingSitesRaw();
        foreach ($allSites as $site) {
            $mappingRootPageToSite[$site->getRootPageId()] = $site;
        }

        $parsedBody = $request->getParsedBody();
        $returnUrl = $this->resolveReturnUrl($request);

        if (isset($parsedBody['closeDoc']) && (int)$parsedBody['closeDoc'] === 1) {
            // Closing means no save, just redirect to overview
            return new RedirectResponse($returnUrl);
        }
        $isSave = $parsedBody['_savedok'] ?? false;
        $isSaveClose = $parsedBody['_saveandclosedok'] ?? false;
        if (!$isSave && !$isSaveClose) {
            throw new \RuntimeException('Either save or save and close', 1520370364);
        }

        if (!isset($parsedBody['data']['site']) || !is_array($parsedBody['data']['site'])) {
            throw new \RuntimeException('No site data or site identifier given', 1521030950);
        }

        $data = $parsedBody['data'];
        // This can be NEW123 for new records
        $unprocessedPageId = key($data['site']);
        $isRootPageIdPlaceholder = $this->envPlaceholderProcessor->canProcess((string)$unprocessedPageId);
        $pageId = $isRootPageIdPlaceholder
            ? (int)$this->envPlaceholderProcessor->process($unprocessedPageId)
            : (int)$unprocessedPageId;

        $sysSiteRow = current($data['site']);
        $siteIdentifier = $sysSiteRow['identifier'] ?? '';

        $isNewConfiguration = false;
        $currentIdentifier = '';
        if (isset($mappingRootPageToSite[$pageId])) {
            $currentSite = $mappingRootPageToSite[$pageId];
            $currentSiteConfiguration = $currentSite->getConfiguration();
            $currentIdentifier = $currentSite->getIdentifier();
        } else {
            $currentSiteConfiguration = [];
            $isNewConfiguration = true;
            $pageId = (int)$parsedBody['rootPageId'];
            if ($pageId <= 0) {
                // Early validation of rootPageId - it must always be given and greater than 0
                throw new \RuntimeException('No root page id found', 1521719709);
            }
        }

        $siteTca = $this->siteTcaConfiguration->getTca();
        // Validate site identifier and do not store or further process it
        $siteIdentifier = $this->validateAndProcessIdentifier($isNewConfiguration, $siteIdentifier, $pageId, $allSites, $mappingRootPageToSite, $siteTca);
        unset($sysSiteRow['identifier']);

        try {
            $newSysSiteData = [];
            // Hard set rootPageId: This is TCA readOnly and not transmitted by FormEngine, but is also the "uid" of the site record
            $newSysSiteData['rootPageId'] = $isRootPageIdPlaceholder ? $unprocessedPageId : $pageId;
            foreach ($sysSiteRow as $fieldName => $fieldValue) {
                $type = $siteTca['site']['columns'][$fieldName]['config']['type'];
                $renderType = $siteTca['site']['columns'][$fieldName]['config']['renderType'] ?? '';
                switch ($type) {
                    case 'input':
                    case 'number':
                    case 'email':
                    case 'link':
                    case 'datetime':
                    case 'color':
                    case 'text':
                        $fieldValue = $this->validateAndProcessValue('site', $fieldName, $fieldValue, $siteTca);
                        $newSysSiteData[$fieldName] = $fieldValue;
                        break;

                    case 'inline':
                        $newSysSiteData[$fieldName] = [];
                        $childRowIds = GeneralUtility::trimExplode(',', $fieldValue, true);
                        if (!isset($siteTca['site']['columns'][$fieldName]['config']['foreign_table'])) {
                            throw new \RuntimeException('No foreign_table found for inline type', 1521555037);
                        }
                        $foreignTable = $siteTca['site']['columns'][$fieldName]['config']['foreign_table'];
                        foreach ($childRowIds as $childRowId) {
                            $childRowData = [];
                            if (!isset($data[$foreignTable][$childRowId])) {
                                if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) {
                                    // A collapsed inline record: Fetch data from existing config
                                    $newSysSiteData[$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId];
                                    continue;
                                }
                                throw new \RuntimeException('No data found for table ' . $foreignTable . ' with id ' . $childRowId, 1521555177);
                            }
                            $childRow = $data[$foreignTable][$childRowId];
                            foreach ($childRow as $childFieldName => $childFieldValue) {
                                if ($childFieldName === 'pid') {
                                    // pid is added by inline by default, but not relevant for yml storage
                                    continue;
                                }
                                $type = $siteTca[$foreignTable]['columns'][$childFieldName]['config']['type'];
                                switch ($type) {
                                    case 'input':
                                    case 'number':
                                    case 'email':
                                    case 'link':
                                    case 'datetime':
                                    case 'color':
                                    case 'select':
                                    case 'text':
                                        $childRowData[$childFieldName] = $childFieldValue;
                                        break;
                                    case 'check':
                                        $childRowData[$childFieldName] = (bool)$childFieldValue;
                                        break;
                                    default:
                                        throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521555340);
                                }
                            }
                            $newSysSiteData[$fieldName][] = $childRowData;
                        }
                        break;

                    case 'siteLanguage':
                        if (!isset($siteTca['site_language'])) {
                            throw new \RuntimeException('Required foreign table site_language does not exist', 1624286811);
                        }
                        if (!isset($siteTca['site_language']['columns']['languageId'])
                            || ($siteTca['site_language']['columns']['languageId']['config']['type'] ?? '') !== 'select'
                        ) {
                            throw new \RuntimeException(
                                'Required foreign field languageId does not exist or is not of type select',
                                1624286812
                            );
                        }
                        $newSysSiteData[$fieldName] = [];
                        $lastLanguageId = $this->getLastLanguageId();
                        foreach (GeneralUtility::trimExplode(',', $fieldValue, true) as $childRowId) {
                            if (!isset($data['site_language'][$childRowId])) {
                                if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) {
                                    $newSysSiteData[$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId];
                                    continue;
                                }
                                throw new \RuntimeException('No data found for table site_language with id ' . $childRowId, 1624286813);
                            }
                            $childRowData = [];
                            foreach ($data['site_language'][$childRowId] ?? [] as $childFieldName => $childFieldValue) {
                                if ($childFieldName === 'pid') {
                                    // pid is added by default, but not relevant for yml storage
                                    continue;
                                }
                                if ($childFieldName === 'languageId'
                                    && (int)$childFieldValue === PHP_INT_MAX
                                    && str_starts_with($childRowId, 'NEW')
                                ) {
                                    // In case we deal with a new site language, whose "languageID" field is
                                    // set to the PHP_INT_MAX placeholder, the next available language ID has
                                    // to be used (auto-increment).
                                    $childRowData[$childFieldName] = ++$lastLanguageId;
                                    continue;
                                }
                                $type = $siteTca['site_language']['columns'][$childFieldName]['config']['type'];
                                switch ($type) {
                                    case 'input':
                                    case 'number':
                                    case 'email':
                                    case 'link':
                                    case 'datetime':
                                    case 'color':
                                    case 'select':
                                    case 'text':
                                        $childRowData[$childFieldName] = $childFieldValue;
                                        break;
                                    case 'check':
                                        $childRowData[$childFieldName] = (bool)$childFieldValue;
                                        break;
                                    default:
                                        throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1624286814);
                                }
                            }
                            $newSysSiteData[$fieldName][] = $childRowData;
                        }
                        break;

                    case 'select':
                        if ($renderType === 'selectMultipleSideBySide') {
                            $fieldValues = is_array($fieldValue) ? $fieldValue : GeneralUtility::trimExplode(',', $fieldValue, true);
                            $newSysSiteData[$fieldName] = $fieldValues;
                        } else {
                            if (MathUtility::canBeInterpretedAsInteger($fieldValue)) {
                                $fieldValue = (int)$fieldValue;
                            } elseif (is_array($fieldValue)) {
                                $fieldValue = implode(',', $fieldValue);
                            }
                            $newSysSiteData[$fieldName] = $fieldValue;
                        }

                        break;

                    case 'check':
                        $newSysSiteData[$fieldName] = (bool)$fieldValue;
                        break;

                    default:
                        throw new \RuntimeException('TCA type "' . $type . '" is not implemented in site handling', 1521032781);
                }
            }

            $newSiteConfiguration = $this->validateFullStructure(
                $this->getMergeSiteData($currentSiteConfiguration, $newSysSiteData),
                $isNewConfiguration
            );

            // Persist the configuration
            try {
                if (!$isNewConfiguration && $currentIdentifier !== $siteIdentifier) {
                    $this->siteWriter->rename($currentIdentifier, $siteIdentifier);
                    $this->getBackendUser()->writelog(Type::SITE, SiteAction::RENAME, SystemLogErrorClassification::MESSAGE, null, 'Site configuration \'%s\' was renamed to \'%s\'.', [$currentIdentifier, $siteIdentifier], 'site');
                }
                $this->siteWriter->write($siteIdentifier, $newSiteConfiguration, true);
                if ($isNewConfiguration) {
                    $this->getBackendUser()->writelog(Type::SITE, SiteAction::CREATE, SystemLogErrorClassification::MESSAGE, null, 'Site configuration \'%s\' was created.', [$siteIdentifier], 'site');
                } else {
                    $this->getBackendUser()->writelog(Type::SITE, SiteAction::UPDATE, SystemLogErrorClassification::MESSAGE, null, 'Site configuration \'%s\' was updated.', [$siteIdentifier], 'site');
                }
            } catch (SiteConfigurationWriteException $e) {
                $flashMessage = new FlashMessage($e->getMessage(), '', ContextualFeedbackSeverity::WARNING, true);
                $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
                $defaultFlashMessageQueue->enqueue($flashMessage);
            }
        } catch (SiteValidationErrorException $e) {
            // Do not store new config if a validation error is thrown, but redirect only to show a generated flash message
        }

        $saveRoute = $this->uriBuilder->buildUriFromRoute('site_configuration.edit', [
            'site' => $siteIdentifier,
            'returnUrl' => $returnUrl,
        ]);
        if ($isSaveClose) {
            return new RedirectResponse($returnUrl);
        }
        return new RedirectResponse($saveRoute);
    }

    /**
     * Validation and processing of site identifier
     *
     * @param bool $isNew If true, we're dealing with a new record
     * @param string $identifier Given identifier to validate and process
     * @param int $rootPageId Page uid this identifier is bound to
     * @param array<non-empty-string, Site> $allSites All sites loaded without `settings.yaml`.
     * @param array<int, Site> $mappingRootPageToSite Identifier site mapping as lookup. Not loaded `settings.yaml`.
     * @param array $siteTca TCA for site
     * @return mixed Verified / modified value
     */
    protected function validateAndProcessIdentifier(bool $isNew, string $identifier, int $rootPageId, array $allSites, array $mappingRootPageToSite, array $siteTca)
    {
        $languageService = $this->getLanguageService();
        // Normal "eval" processing of field first
        $identifier = $this->validateAndProcessValue('site', 'identifier', $identifier, $siteTca);
        if ($isNew) {
            // Verify no other site with this identifier exists. If so, find a new unique name as
            // identifier and show a flash message the identifier has been adapted
            if (($allSites[$identifier] ?? null) instanceof Site) {
                // Force this identifier to be unique
                $originalIdentifier = $identifier;
                $identifier = StringUtility::getUniqueId($identifier . '-');
                $message = sprintf(
                    $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.message'),
                    $originalIdentifier,
                    $identifier
                );
                $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.title');
                $flashMessage = new FlashMessage($message, $messageTitle, ContextualFeedbackSeverity::WARNING, true);
                $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
                $defaultFlashMessageQueue->enqueue($flashMessage);
            }
        } else {
            // If this is an existing config, the site for this identifier must have the same rootPageId, otherwise
            // a user tried to rename a site identifier to a different site that already exists. If so, we do not rename
            // the site and show a flash message
            $site = ($allSites[$identifier] ?? null);
            if ($site instanceof Site
                && $site->getRootPageId() !== $rootPageId
                && ($mappingRootPageToSite[$rootPageId] ?? null) instanceof Site
            ) {
                // Find original value and keep this
                $origSite = $mappingRootPageToSite[$rootPageId];
                $originalIdentifier = $identifier;
                $identifier = $origSite->getIdentifier();
                $message = sprintf(
                    $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.message'),
                    $originalIdentifier,
                    $identifier
                );
                $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.title');
                $flashMessage = new FlashMessage($message, $messageTitle, ContextualFeedbackSeverity::WARNING, true);
                $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
                $defaultFlashMessageQueue->enqueue($flashMessage);
            }
        }
        return $identifier;
    }

    /**
     * Simple validation and processing method for incoming form field values.
     *
     * Note this does not support all TCA "eval" options but only what we really need.
     *
     * @param string $tableName Table name
     * @param string $fieldName Field name
     * @param mixed $fieldValue Incoming value from FormEngine
     * @param array $siteTca TCA for site
     * @return mixed Verified / modified value
     * @throws SiteValidationErrorException
     * @throws \RuntimeException
     */
    protected function validateAndProcessValue(string $tableName, string $fieldName, $fieldValue, array $siteTca)
    {
        $languageService = $this->getLanguageService();
        $fieldConfig = $siteTca[$tableName]['columns'][$fieldName]['config'];
        $handledEvals = [];

        if (!$this->validateValueForRequired($fieldConfig, $fieldValue)) {
            // Validation throws - these should be handled client side already,
            // eg. 'required' being set and receiving empty, shouldn't happen server side
            $message = sprintf(
                $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.message'),
                $fieldName
            );
            $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.title');
            $flashMessage = new FlashMessage($message, $messageTitle, ContextualFeedbackSeverity::WARNING, true);
            $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
            $defaultFlashMessageQueue->enqueue($flashMessage);
            throw new SiteValidationErrorException(
                'Field ' . $fieldName . ' is set to required, but received empty.',
                1521726421
            );
        }

        if (!empty($fieldConfig['eval'])) {
            $evalArray = GeneralUtility::trimExplode(',', $fieldConfig['eval'], true);
            // Processing
            if (in_array('alphanum_x', $evalArray, true)) {
                $handledEvals[] = 'alphanum_x';
                $fieldValue = preg_replace('/[^a-zA-Z0-9_-]/', '', $fieldValue);
            }
            if (in_array('lower', $evalArray, true)) {
                $handledEvals[] = 'lower';
                $fieldValue = mb_strtolower($fieldValue, 'utf-8');
            }
            if (in_array('trim', $evalArray, true)) {
                $handledEvals[] = 'trim';
                $fieldValue = trim($fieldValue);
            }
            if (in_array('int', $evalArray, true)) {
                $handledEvals[] = 'int';
                $fieldValue = (int)$fieldValue;
            }
            if (!empty(array_diff($evalArray, $handledEvals))) {
                throw new \RuntimeException('At least one not implemented \'eval\' in list ' . $fieldConfig['eval'], 1522491734);
            }
        }
        if (isset($fieldConfig['range']['lower'])) {
            $fieldValue = (int)$fieldValue < (int)$fieldConfig['range']['lower'] ? (int)$fieldConfig['range']['lower'] : (int)$fieldValue;
        }
        if (isset($fieldConfig['range']['upper'])) {
            $fieldValue = (int)$fieldValue > (int)$fieldConfig['range']['upper'] ? (int)$fieldConfig['range']['upper'] : (int)$fieldValue;
        }
        return $fieldValue;
    }

    /**
     * Last sanitation method after all data has been gathered. Check integrity
     * of full record, manipulate if possible, or throw exception if unfixable broken.
     *
     * @param array $newSysSiteData Incoming data
     * @param bool $isNewConfiguration Flag whether site configuration is new
     * @return array Updated data if needed
     * @throws \RuntimeException
     */
    protected function validateFullStructure(array $newSysSiteData, bool $isNewConfiguration): array
    {
        $languageService = $this->getLanguageService();
        // Verify there are not two error handlers with the same error code
        if (isset($newSysSiteData['errorHandling']) && is_array($newSysSiteData['errorHandling'])) {
            $uniqueCriteria = [];
            $validChildren = [];
            foreach ($newSysSiteData['errorHandling'] as $child) {
                if (!isset($child['errorCode'])) {
                    throw new \RuntimeException('No errorCode found', 1521788518);
                }
                if (!in_array((int)$child['errorCode'], $uniqueCriteria, true)) {
                    $uniqueCriteria[] = (int)$child['errorCode'];
                    $child['errorCode'] = (int)$child['errorCode'];
                    $validChildren[] = $child;
                } else {
                    $message = sprintf(
                        $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.message'),
                        $child['errorCode']
                    );
                    $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.title');
                    $flashMessage = new FlashMessage($message, $messageTitle, ContextualFeedbackSeverity::WARNING, true);
                    $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
                    $defaultFlashMessageQueue->enqueue($flashMessage);
                }
            }
            $newSysSiteData['errorHandling'] = $validChildren;
        }

        // Verify there is at least one site_language element configured.
        if (!isset($newSysSiteData['languages']) || !is_array($newSysSiteData['languages']) || count($newSysSiteData['languages']) < 1) {
            throw new \RuntimeException(
                'No default language definition found. The interface does not allow this. Aborting',
                1521789306
            );
        }
        $uniqueCriteria = [];
        $validChildren = [];
        foreach ($newSysSiteData['languages'] as $child) {
            if (!isset($child['languageId'])) {
                throw new \RuntimeException('languageId not found', 1521789455);
            }
            if (!in_array((int)$child['languageId'], $uniqueCriteria, true)) {
                $uniqueCriteria[] = (int)$child['languageId'];
                $child['languageId'] = (int)$child['languageId'];
                $validChildren[] = $child;
            } else {
                $message = sprintf(
                    $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.message'),
                    $child['languageId']
                );
                $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title');
                $flashMessage = new FlashMessage($message, $messageTitle, ContextualFeedbackSeverity::WARNING, true);
                $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
                $defaultFlashMessageQueue->enqueue($flashMessage);
            }
        }
        // On new site configurations, ensure that the only existing language has the languageId set to 0
        // @todo: this shouldn't be done here, but rather properly handled in saveAction() where 'siteLanguage' is handled
        if ($isNewConfiguration && count($validChildren) === 1) {
            $validChildren[0]['languageId'] = 0;
        }
        $newSysSiteData['languages'] = $validChildren;

        // cleanup configuration
        foreach ($newSysSiteData as $identifier => $value) {
            if (is_array($value) && empty($value)) {
                unset($newSysSiteData[$identifier]);
            }
        }

        return $newSysSiteData;
    }

    /**
     * Delete an existing configuration
     */
    public function deleteAction(ServerRequestInterface $request): ResponseInterface
    {
        $siteIdentifier = $request->getParsedBody()['site'] ?? '';
        if (empty($siteIdentifier)) {
            throw new \RuntimeException('Not site identifier given', 1521565182);
        }
        try {
            // Verify site does exist, method throws if not
            $this->siteWriter->delete($siteIdentifier);
            $this->getBackendUser()->writelog(Type::SITE, SiteAction::DELETE, SystemLogErrorClassification::MESSAGE, null, 'Site configuration \'%s\' was deleted.', [$siteIdentifier], 'site');
        } catch (SiteConfigurationWriteException $e) {
            $flashMessage = new FlashMessage($e->getMessage(), '', ContextualFeedbackSeverity::WARNING, true);
            $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
            $defaultFlashMessageQueue->enqueue($flashMessage);
        }
        $overviewRoute = $this->uriBuilder->buildUriFromRoute('site_configuration');
        return new RedirectResponse($overviewRoute);
    }

    /**
     * Create document header buttons of "detail" action
     */
    protected function configureDetailViewDocHeader(ModuleTemplate $view, ?string $siteIdentifier, ServerRequestInterface $request): void
    {
        // Back button
        if ($returnUrl = $this->resolveReturnUrl($request)) {
            $view->addButtonToButtonBar($this->componentFactory->createBackButton($returnUrl));
        }

        if ($siteIdentifier) {
            // 'Edit site configuration' button
            $editSiteConfigurationButton = $this->componentFactory->createLinkButton()
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:edit.site_configuration'))
                ->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL))
                ->setShowLabelText(true)
                ->setHref((string)$this->uriBuilder->buildUriFromRoute('site_configuration.edit', [
                    'site' => $siteIdentifier,
                    'returnUrl' => $this->uriBuilder->buildUriFromRoute('site_configuration.detail', [
                        'site' => $siteIdentifier,
                    ]),
                ]));
            $view->addButtonToButtonBar($editSiteConfigurationButton, ButtonBar::BUTTON_POSITION_LEFT, 3);

            // 'Edit site settings' button
            $editSiteSettingsButton = $this->componentFactory->createLinkButton()
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:edit.editSiteSettings'))
                ->setIcon($this->iconFactory->getIcon('actions-cog', IconSize::SMALL))
                ->setShowLabelText(true)
                ->setHref((string)$this->uriBuilder->buildUriFromRoute('site_configuration.editSettings', [
                    'site' => $siteIdentifier,
                    'returnUrl' => $this->uriBuilder->buildUriFromRoute('site_configuration.detail', [
                        'site' => $siteIdentifier,
                    ]),
                ]));
            $view->addButtonToButtonBar($editSiteSettingsButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
        }

        // Set shortcut context - reload button is added automatically
        $view->getDocHeaderComponent()->setShortcutContext(
            'site_configuration.detail',
            sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:labels.detail'), $siteIdentifier),
            ['site' => $siteIdentifier]
        );
    }

    /**
     * Create document header buttons of "edit" action
     */
    protected function configureEditViewDocHeader(ModuleTemplate $view, ?string $siteIdentifier, string $documentTitle = ''): void
    {
        $lang = $this->getLanguageService();
        $closeButton = $this->componentFactory->createCloseButton('#')
            ->setClasses('t3js-editform-close');
        $saveButton = $this->componentFactory->createSaveButton('siteConfigurationController');
        $view->addButtonToButtonBar($closeButton);
        $view->addButtonToButtonBar($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
        if ($siteIdentifier) {
            $editSiteSettingsButton = $this->componentFactory->createLinkButton()
                ->setTitle($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:edit.editSiteSettings'))
                ->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL))
                ->setShowLabelText(true)
                ->setHref((string)$this->uriBuilder->buildUriFromRoute('site_configuration.editSettings', [
                    'site' => $siteIdentifier,
                    'returnUrl' => $this->uriBuilder->buildUriFromRoute('site_configuration.edit', [
                        'site' => $siteIdentifier,
                    ]),
                ]));
            $view->addButtonToButtonBar($editSiteSettingsButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
        }
        // Set shortcut context - reload button is added automatically
        $view->getDocHeaderComponent()->setShortcutContext(
            'site_configuration.edit',
            $documentTitle,
            ['site' => $siteIdentifier],
        );
    }

    /**
     * Resolves the document title used for the browser tab and shortcut.
     */
    protected function resolveDocumentTitle(LanguageService $languageService, bool $isNewConfig, ?string $siteIdentifier): string
    {
        $typeLabel = $languageService->sL('backend.siteconfiguration:edit.typeLabel');
        if ($isNewConfig) {
            return $languageService->sL('backend.siteconfiguration:edit.createNewSite');
        }
        return implode(' · ', array_filter([$siteIdentifier, $typeLabel]));
    }

    /**
     * View mode
     */
    protected function addDocHeaderViewModeButton(ModuleTemplate $moduleTemplate, SetupModuleViewMode $viewMode): void
    {
        $languageService = $this->getLanguageService();
        $viewModeButton = $this->componentFactory->createDropDownButton()
            ->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view'))
            ->setIcon($this->iconFactory->getIcon('actions-cog'))
            ->setShowLabelText(true);

        $viewModeButton->addItem(
            $this->componentFactory->createDropDownRadio()
            ->setActive(($viewMode === SetupModuleViewMode::TILES))
            ->setHref(
                (string)$this->uriBuilder->buildUriFromRoute(
                    'site_configuration',
                    [
                        'viewMode' => SetupModuleViewMode::TILES->value,
                    ]
                )
            )
            ->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.tiles'))
            ->setIcon($this->iconFactory->getIcon('actions-viewmode-tiles', IconSize::SMALL))
        );

        $viewModeButton->addItem(
            $this->componentFactory->createDropDownRadio()
            ->setActive(($viewMode === SetupModuleViewMode::LIST))
            ->setHref(
                (string)$this->uriBuilder->buildUriFromRoute(
                    'site_configuration',
                    [
                        'viewMode' => SetupModuleViewMode::LIST->value,
                    ]
                )
            )
            ->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.list'))
            ->setIcon($this->iconFactory->getIcon('actions-viewmode-list', IconSize::SMALL))
        );

        $moduleTemplate->addButtonToButtonBar($viewModeButton, ButtonBar::BUTTON_POSITION_RIGHT, 2);
    }

    /**
     * Returns a list of pages that have 'is_siteroot' set
     * or are on pid 0 and not in list of excluded doktypes
     */
    protected function getAllSitePages(): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
        $queryBuilder->getRestrictions()->removeAll();
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, 0));
        $statement = $queryBuilder
            ->select('*')
            ->from('pages')
            ->where(
                $queryBuilder->expr()->or(
                    $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter(\Local\Multilanguage\Service\DefaultLanguageTagService::getTag())),
                    $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter('')),
                ),
                $queryBuilder->expr()->or(
                    $queryBuilder->expr()->and(
                        $queryBuilder->expr()->eq('pid', 0),
                        $queryBuilder->expr()->notIn('doktype', [
                            PageRepository::DOKTYPE_SYSFOLDER,
                            PageRepository::DOKTYPE_SPACER,
                            PageRepository::DOKTYPE_LINK,
                        ])
                    ),
                    $queryBuilder->expr()->eq('is_siteroot', 1)
                )
            )
            ->orderBy('pid')
            ->addOrderBy('sorting')
            ->executeQuery();

        $pages = [];
        while ($row = $statement->fetchAssociative()) {
            $row['rootline'] = BackendUtility::BEgetRootLine((int)$row['uid']);
            array_pop($row['rootline']);
            $row['rootline'] = array_reverse($row['rootline']);
            $pages[(int)$row['uid']] = $row;
        }
        return $pages;
    }

    /**
     * Get all entry duplicates which are used multiple times
     *
     * @param Site[] $allSites
     */
    protected function getDuplicatedEntryPoints(array $allSites, array $pages): array
    {
        $duplicatedEntryPoints = [];

        foreach ($allSites as $site) {
            if (!isset($pages[$site->getRootPageId()])) {
                continue;
            }
            foreach ($site->getAllLanguages() as $language) {
                $base = $language->getBase();
                $entryPoint = rtrim((string)$language->getBase(), '/');
                $scheme = $base->getScheme() ? $base->getScheme() . '://' : '//';
                $entryPointWithoutScheme = str_replace($scheme, '', $entryPoint);
                if (!isset($duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint])) {
                    $duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint] = 1;
                } else {
                    $duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint]++;
                }
            }
        }
        return array_filter($duplicatedEntryPoints, static function (array $variants): bool {
            return count($variants) > 1 || reset($variants) > 1;
        }, ARRAY_FILTER_USE_BOTH);
    }

    /**
     * Returns the last (highest) language id from all sites
     */
    protected function getLastLanguageId(): int
    {
        $lastLanguageId = 0;
        foreach ($this->siteFinder->getAllSites() as $site) {
            foreach ($site->getAllLanguages() as $language) {
                if ($language->getLanguageId() > $lastLanguageId) {
                    $lastLanguageId = $language->getLanguageId();
                }
            }
        }
        return $lastLanguageId;
    }

    /**
     * Checks if required=TRUE is set.
     * If set: checks if the value is not empty (or not "0").
     * If not set or set to FALSE: Returns TRUE.
     */
    protected function validateValueForRequired(array $tcaFieldConfig, mixed $value): bool
    {
        if (!($tcaFieldConfig['required'] ?? false)) {
            return true;
        }

        return !empty($value) || $value === '0';
    }

    /**
     * Method keeps root config objects, which are not given via GUI. This way,
     * extension authors are able to use their own objects on root level that are
     * not configurable via GUI. However: We overwrite the full subset of any GUI
     * object to make sure we have a clean state.
     *
     * Additionally, we also keep the baseVariants of languages, since they
     * can't be modified via the GUI, but are part of the public API.
     */
    protected function getMergeSiteData(array $currentSiteConfiguration, array $newSysSiteData): array
    {
        $newSysSiteData = array_merge($currentSiteConfiguration, $newSysSiteData);

        // @todo: this should go away, once base variants for languages are managable via the GUI.
        $existingLanguageConfigurationsWithBaseVariants = [];
        $existingLanguagesWithLegacyProperties = [];
        foreach ($currentSiteConfiguration['languages'] ?? [] as $languageConfiguration) {
            if (isset($languageConfiguration['baseVariants'])) {
                $existingLanguageConfigurationsWithBaseVariants[$languageConfiguration['languageId']] = $languageConfiguration['baseVariants'];
            }
            if (isset($languageConfiguration['typo3Language'])) {
                $existingLanguagesWithLegacyProperties[$languageConfiguration['languageId']]['typo3Language'] = $languageConfiguration['typo3Language'];
            }
            if (isset($languageConfiguration['iso-639-1'])) {
                $existingLanguagesWithLegacyProperties[$languageConfiguration['languageId']]['iso-639-1'] = $languageConfiguration['iso-639-1'];
            }
            if (isset($languageConfiguration['direction'])) {
                $existingLanguagesWithLegacyProperties[$languageConfiguration['languageId']]['direction'] = $languageConfiguration['direction'];
            }
        }
        foreach ($newSysSiteData['languages'] ?? [] as $key => $languageConfiguration) {
            $languageId = $languageConfiguration['languageId'];
            if (isset($existingLanguageConfigurationsWithBaseVariants[$languageId])) {
                $newSysSiteData['languages'][$key]['baseVariants'] = $existingLanguageConfigurationsWithBaseVariants[$languageId];
            }
            foreach ($existingLanguagesWithLegacyProperties[$languageId] ?? [] as $propertyName => $propertyValue) {
                $newSysSiteData['languages'][$key][$propertyName] = $propertyValue;
            }
        }

        return $newSysSiteData;
    }

    private function resolveSettingLabels(SettingDefinition $definition): SettingDefinition
    {
        $languageService = $this->getLanguageService();
        return new SettingDefinition(...[
            ...get_object_vars($definition),
            'label' => $languageService->sL($definition->label),
            'description' => $definition->description !== null ? $languageService->sL($definition->description) : null,
            'enum' => array_map(
                static fn(string|int|float|bool $label): string => $languageService->sL((string)$label),
                $definition->enum
            ),
        ]);
    }

    protected function resolveReturnUrl(ServerRequestInterface $request): string
    {
        return GeneralUtility::sanitizeLocalUrl(
            (string)($request->getParsedBody()['returnUrl'] ?? $request->getQueryParams()['returnUrl'] ?? ''),
            $request
        ) ?: (string)$this->uriBuilder->buildUriFromRoute('site_configuration');
    }

    protected function getLanguageService(): LanguageService
    {
        return $GLOBALS['LANG'];
    }

    protected function getBackendUser(): BackendUserAuthentication
    {
        return $GLOBALS['BE_USER'];
    }
}
