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

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Core\ClassLoadingInformation;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\ReferenceIndex;
use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
use TYPO3\CMS\Core\Database\Schema\SqlReader;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Localization\LanguagePackService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator;
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
use TYPO3\CMS\Core\Service\OpcodeCacheService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Service\ClearCacheService;
use TYPO3\CMS\Install\Service\ClearTableService;
use TYPO3\CMS\Install\Service\LateBootService;
use TYPO3\CMS\Install\Service\Typo3tempFileService;

/**
 * Maintenance controller
 * @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
 */
class MaintenanceController extends AbstractController
{
    protected PasswordPolicyValidator $passwordPolicyValidator;

    public function __construct(
        private readonly LateBootService $lateBootService,
        private readonly ClearCacheService $clearCacheService,
        private readonly ConfigurationManager $configurationManager,
        private readonly PasswordHashFactory $passwordHashFactory,
        private readonly Locales $locales,
        private readonly LanguageServiceFactory $languageServiceFactory,
        private readonly FormProtectionFactory $formProtectionFactory,
    ) {
        $GLOBALS['LANG'] = $this->languageServiceFactory->create('en');
        $passwordPolicy = $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default';
        $this->passwordPolicyValidator = GeneralUtility::makeInstance(
            PasswordPolicyValidator::class,
            PasswordPolicyAction::NEW_USER_PASSWORD,
            is_string($passwordPolicy) ? $passwordPolicy : ''
        );
    }

    /**
     * Main "show the cards" view
     */
    public function cardsAction(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->initializeView($request);
        return new JsonResponse([
            'success' => true,
            'html' => $view->render('Maintenance/Cards'),
        ]);
    }

    /**
     * Clear cache framework and opcode caches
     */
    public function cacheClearAllAction(): ResponseInterface
    {
        $this->clearCacheService->clearAll();
        GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
        $messageQueue = new FlashMessageQueue('install');
        $messageQueue->enqueue(
            new FlashMessage('Successfully cleared all caches and all available opcode caches.', 'Caches cleared')
        );
        return new JsonResponse([
            'success' => true,
            'status' => $messageQueue,
        ]);
    }

    /**
     * Clear typo3temp files statistics action
     */
    public function clearTypo3tempFilesStatsAction(ServerRequestInterface $request): ResponseInterface
    {
        $container = $this->lateBootService->loadExtLocalconfDatabase(false);
        $typo3tempFileService = $container->get(Typo3tempFileService::class);

        $view = $this->initializeView($request);
        $formProtection = $this->formProtectionFactory->createFromRequest($request);
        $view->assignMultiple([
            'clearTypo3tempFilesToken' => $formProtection->generateToken('installTool', 'clearTypo3tempFiles'),
        ]);
        return new JsonResponse(
            [
                'success' => true,
                'stats' => $typo3tempFileService->getDirectoryStatistics(),
                'html' => $view->render('Maintenance/ClearTypo3tempFiles'),
                'buttons' => [
                    [
                        'btnClass' => 'btn-default t3js-clearTypo3temp-stats',
                        'text' => 'Scan again',
                    ],
                ],
            ]
        );
    }

    /**
     * Clear typo3temp/assets or FAL processed Files
     */
    public function clearTypo3tempFilesAction(ServerRequestInterface $request): ResponseInterface
    {
        $container = $this->lateBootService->loadExtLocalconfDatabase(false);
        $typo3tempFileService = $container->get(Typo3tempFileService::class);
        $messageQueue = new FlashMessageQueue('install');
        $folder = $request->getParsedBody()['install']['folder'];
        // storageUid is an optional post param if FAL storages should be cleaned
        $storageUid = $request->getParsedBody()['install']['storageUid'] ?? null;
        if ($storageUid === null) {
            $typo3tempFileService->clearAssetsFolder($folder);
            $messageQueue->enqueue(new FlashMessage('The directory "' . $folder . '" has been cleared successfully', 'Directory cleared'));
        } else {
            $storageUid = (int)$storageUid;
            // We have to get the stats before deleting files, otherwise we're not able to retrieve the amount of files anymore
            $stats = $typo3tempFileService->getStatsFromStorageByUid($storageUid);
            $failedDeletions = $typo3tempFileService->clearProcessedFiles($storageUid);
            if ($failedDeletions) {
                $messageQueue->enqueue(new FlashMessage(
                    'Failed to delete ' . $failedDeletions . ' processed files. See TYPO3 log (by default typo3temp/var/log/typo3_*.log)',
                    'Failed to delete files',
                    ContextualFeedbackSeverity::ERROR
                ));
            } else {
                $messageQueue->enqueue(new FlashMessage(
                    sprintf('Removed %d files from directory "%s"', $stats['numberOfFiles'], $stats['directory']),
                    'Deleted processed files'
                ));
            }
        }
        return new JsonResponse([
            'success' => true,
            'status' => $messageQueue,
        ]);
    }

    /**
     * Dump autoload information
     */
    public function dumpAutoloadAction(): ResponseInterface
    {
        $messageQueue = new FlashMessageQueue('install');
        if (Environment::isComposerMode()) {
            $messageQueue->enqueue(new FlashMessage(
                'Skipped generating additional class loading information in Composer mode.',
                'Autoloader not dumped',
                ContextualFeedbackSeverity::NOTICE
            ));
        } else {
            ClassLoadingInformation::dumpClassLoadingInformation();
            $messageQueue->enqueue(new FlashMessage(
                'Successfully dumped class loading information for extensions.',
                'Dumped autoloader'
            ));
        }
        return new JsonResponse([
            'success' => true,
            'status' => $messageQueue,
        ]);
    }

    /**
     * Get main database analyzer modal HTML
     */
    public function databaseAnalyzerAction(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->initializeView($request);
        $formProtection = $this->formProtectionFactory->createFromRequest($request);
        $view->assignMultiple([
            'databaseAnalyzerExecuteToken' => $formProtection->generateToken('installTool', 'databaseAnalyzerExecute'),
        ]);
        return new JsonResponse([
            'success' => true,
            'html' => $view->render('Maintenance/DatabaseAnalyzer'),
            'buttons' => [
                [
                    'btnClass' => 'btn-default t3js-databaseAnalyzer-analyze',
                    'text' => 'Run database compare again',
                ], [
                    'btnClass' => 'btn-warning t3js-databaseAnalyzer-execute',
                    'text' => 'Apply selected changes',
                ],
            ],
        ]);
    }

    /**
     * Analyze current database situation
     */
    public function databaseAnalyzerAnalyzeAction(ServerRequestInterface $request): ResponseInterface
    {
        $container = $this->lateBootService->loadExtLocalconfDatabase();
        $schemaMigrator = $container->get(SchemaMigrator::class);

        $messageQueue = new FlashMessageQueue('install');
        $suggestions = [];
        try {
            $sqlReader = $container->get(SqlReader::class);
            $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
            $addCreateChange = $schemaMigrator->getUpdateSuggestions($sqlStatements);

            // Aggregate the per-connection statements into one flat array
            $addCreateChange = array_merge_recursive(...array_values($addCreateChange));
            if (!empty($addCreateChange['create_table'])) {
                $suggestion = [
                    'key' => 'addTable',
                    'label' => 'Add tables',
                    'enabled' => true,
                    'children' => [],
                ];
                foreach ($addCreateChange['create_table'] as $hash => $statement) {
                    $suggestion['children'][] = [
                        'hash' => $hash,
                        'statement' => $statement,
                    ];
                }
                $suggestions[] = $suggestion;
            }
            if (!empty($addCreateChange['add'])) {
                $suggestion = [
                    'key' => 'addField',
                    'label' => 'Add fields to tables',
                    'enabled' => true,
                    'children' => [],
                ];
                foreach ($addCreateChange['add'] as $hash => $statement) {
                    $suggestion['children'][] = [
                        'hash' => $hash,
                        'statement' => $statement,
                    ];
                }
                $suggestions[] = $suggestion;
            }
            if (!empty($addCreateChange['change'])) {
                $suggestion = [
                    'key' => 'change',
                    'label' => 'Change fields',
                    'enabled' => false,
                    'children' => [],
                ];
                foreach ($addCreateChange['change'] as $hash => $statement) {
                    $child = [
                        'hash' => $hash,
                        'statement' => $statement,
                    ];
                    if (isset($addCreateChange['change_currentValue'][$hash])) {
                        $child['current'] = $addCreateChange['change_currentValue'][$hash];
                    }
                    $suggestion['children'][] = $child;
                }
                $suggestions[] = $suggestion;
            }

            // Difference from current to expected
            $dropRename = $schemaMigrator->getUpdateSuggestions($sqlStatements, true);

            // Aggregate the per-connection statements into one flat array
            $dropRename = array_merge_recursive(...array_values($dropRename));
            if (!empty($dropRename['change_table'])) {
                $suggestion = [
                    'key' => 'renameTableToUnused',
                    'label' => 'Remove tables (rename with prefix)',
                    'enabled' => false,
                    'children' => [],
                ];
                foreach ($dropRename['change_table'] as $hash => $statement) {
                    $child = [
                        'hash' => $hash,
                        'statement' => $statement,
                    ];
                    if (!empty($dropRename['tables_count'][$hash])) {
                        $child['rowCount'] = $dropRename['tables_count'][$hash];
                    }
                    $suggestion['children'][] = $child;
                }
                $suggestions[] = $suggestion;
            }
            if (!empty($dropRename['change'])) {
                $suggestion = [
                    'key' => 'renameTableFieldToUnused',
                    'label' => 'Remove unused fields (rename with prefix)',
                    'enabled' => false,
                    'children' => [],
                ];
                foreach ($dropRename['change'] as $hash => $statement) {
                    $suggestion['children'][] = [
                        'hash' => $hash,
                        'statement' => $statement,
                    ];
                }
                $suggestions[] = $suggestion;
            }
            if (!empty($dropRename['drop'])) {
                $suggestion = [
                    'key' => 'deleteField',
                    'label' => 'Drop fields (really!)',
                    'enabled' => false,
                    'children' => [],
                ];
                foreach ($dropRename['drop'] as $hash => $statement) {
                    $suggestion['children'][] = [
                        'hash' => $hash,
                        'statement' => $statement,
                    ];
                }
                $suggestions[] = $suggestion;
            }
            if (!empty($dropRename['drop_table'])) {
                $suggestion = [
                    'key' => 'deleteTable',
                    'label' => 'Drop tables (really!)',
                    'enabled' => false,
                    'children' => [],
                ];
                foreach ($dropRename['drop_table'] as $hash => $statement) {
                    $child = [
                        'hash' => $hash,
                        'statement' => $statement,
                    ];
                    if (!empty($dropRename['tables_count'][$hash])) {
                        $child['rowCount'] = $dropRename['tables_count'][$hash];
                    }
                    $suggestion['children'][] = $child;
                }
                $suggestions[] = $suggestion;
            }
        } catch (StatementException $e) {
            $messageQueue->enqueue(new FlashMessage(
                $e->getMessage(),
                'Database analysis failed',
                ContextualFeedbackSeverity::ERROR
            ));
        }
        return new JsonResponse([
            'success' => true,
            'status' => $messageQueue,
            'suggestions' => $suggestions,
        ]);
    }

    /**
     * Apply selected database changes
     */
    public function databaseAnalyzerExecuteAction(ServerRequestInterface $request): ResponseInterface
    {
        $container = $this->lateBootService->loadExtLocalconfDatabase();
        $messageQueue = new FlashMessageQueue('install');
        $selectedHashes = $request->getParsedBody()['install']['hashes'] ?? [];
        if (empty($selectedHashes)) {
            $messageQueue->enqueue(new FlashMessage(
                'Please select any change by activating their respective checkboxes.',
                'No database changes selected',
                ContextualFeedbackSeverity::WARNING
            ));
        } else {
            $sqlReader = $container->get(SqlReader::class);
            $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
            $statementHashesToPerform = array_flip($selectedHashes);
            $schemaMigrator = $container->get(SchemaMigrator::class);
            $results = $schemaMigrator->migrate($sqlStatements, $statementHashesToPerform);
            // Create error flash messages if any
            foreach ($results as $errorMessage) {
                $messageQueue->enqueue(new FlashMessage(
                    'Error: ' . $errorMessage,
                    'Database update failed',
                    ContextualFeedbackSeverity::ERROR
                ));
            }
            $messageQueue->enqueue(new FlashMessage(
                'Executed database updates',
                'Executed database updates'
            ));
        }
        return new JsonResponse([
            'success' => true,
            'status' => $messageQueue,
        ]);
    }

    /**
     * Clear table overview statistics action
     */
    public function clearTablesStatsAction(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->initializeView($request);
        $formProtection = $this->formProtectionFactory->createFromRequest($request);
        $view->assignMultiple([
            'clearTablesClearToken' => $formProtection->generateToken('installTool', 'clearTablesClear'),
        ]);
        $container = $this->lateBootService->getContainer(true);
        $clearTableService = $container->get(ClearTableService::class);
        return new JsonResponse([
            'success' => true,
            'stats' => $clearTableService->getTableStatistics(),
            'html' => $view->render('Maintenance/ClearTables'),
            'buttons' => [
                [
                    'btnClass' => 'btn-default t3js-clearTables-stats',
                    'text' => 'Scan again',
                ],
            ],
        ]);
    }

    /**
     * Truncate a specific table
     *
     * @throws \RuntimeException
     */
    public function clearTablesClearAction(ServerRequestInterface $request): ResponseInterface
    {
        $table = $request->getParsedBody()['install']['table'];
        if (empty($table)) {
            throw new \RuntimeException(
                'No table name given',
                1501944076
            );
        }
        $container = $this->lateBootService->getContainer(true);
        $clearTableService = $container->get(ClearTableService::class);
        $clearTableService->clearSelectedTable($table);
        $messageQueue = new FlashMessageQueue('install');
        $messageQueue->enqueue(
            new FlashMessage('The table ' . $table . ' has been cleared.', 'Table cleared')
        );
        return new JsonResponse([
            'success' => true,
            'status' => $messageQueue,
        ]);
    }
    /**
     * Create Admin Get Data action
     */
    public function createAdminGetDataAction(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->initializeView($request);
        $formProtection = $this->formProtectionFactory->createFromRequest($request);
        $view->assignMultiple([
            'createAdminToken' => $formProtection->generateToken('installTool', 'createAdmin'),
            'passwordPolicyRequirements' => $this->passwordPolicyValidator->getRequirements(),
        ]);
        return new JsonResponse([
            'success' => true,
            'html' => $view->render('Maintenance/CreateAdmin'),
            'buttons' => [
                [
                    'btnClass' => 'btn-default t3js-createAdmin-create',
                    'text' => 'Create administrator user',
                ],
            ],
        ]);
    }

    /**
     * Create a backend administrator from given username and password
     */
    public function createAdminAction(ServerRequestInterface $request): ResponseInterface
    {
        $userCreated = false;
        $username = preg_replace('/\\s/i', '', $request->getParsedBody()['install']['userName']);
        $password = $request->getParsedBody()['install']['userPassword'];
        $passwordCheck = $request->getParsedBody()['install']['userPasswordCheck'];
        $email = $request->getParsedBody()['install']['userEmail'] ?? '';
        $realName = $request->getParsedBody()['install']['realName'] ?? '';
        $isSystemMaintainer = ((bool)$request->getParsedBody()['install']['userSystemMaintainer'] == '1') ? true : false;

        $messages = new FlashMessageQueue('install');
        $contextData = new ContextData(newUsername: $username);

        if ($username === '') {
            $messages->enqueue(new FlashMessage(
                'No username given.',
                'Administrator user not created',
                ContextualFeedbackSeverity::ERROR
            ));
        } elseif ($password !== $passwordCheck) {
            $messages->enqueue(new FlashMessage(
                'Passwords do not match.',
                'Administrator user not created',
                ContextualFeedbackSeverity::ERROR
            ));
        } elseif (!$this->passwordPolicyValidator->isValidPassword($password, $contextData)) {
            $messages->enqueue(new FlashMessage(
                'The password does not meet the password policy requirements.',
                'Administrator user not created',
                ContextualFeedbackSeverity::ERROR
            ));
        } else {
            $container = $this->lateBootService->getContainer(true);
            $connectionPool = $container->get(ConnectionPool::class);
            $userExists = $connectionPool->getConnectionForTable('be_users')
                ->count(
                    'uid',
                    'be_users',
                    ['username' => $username]
                );
            if ($userExists) {
                $messages->enqueue(new FlashMessage(
                    'A user with username "' . $username . '" exists already.',
                    'Administrator user not created',
                    ContextualFeedbackSeverity::ERROR
                ));
            } else {
                $hashInstance = $this->passwordHashFactory->getDefaultHashInstance('BE');
                $hashedPassword = $hashInstance->getHashedPassword($password);
                $adminUserFields = [
                    'username' => $username,
                    'password' => $hashedPassword,
                    'admin' => 1,
                    'realName' => $realName,
                    'tstamp' => $GLOBALS['EXEC_TIME'],
                    'crdate' => $GLOBALS['EXEC_TIME'],
                ];
                if (GeneralUtility::validEmail($email)) {
                    $adminUserFields['email'] = $email;
                }
                $connectionPool->getConnectionForTable('be_users')->insert('be_users', $adminUserFields);
                $userCreated = true;

                if ($isSystemMaintainer) {
                    // Get the new admin user uid just created
                    $newAdminUserUid = (int)$connectionPool->getConnectionForTable('be_users')->lastInsertId();

                    // Get the list of the existing systemMaintainer
                    $existingSystemMaintainersList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];

                    // Add the new admin user to the existing systemMaintainer list
                    $newSystemMaintainersList = $existingSystemMaintainersList;
                    $newSystemMaintainersList[] = $newAdminUserUid;

                    // Update the system/settings.php file with the new list
                    $this->configurationManager->setLocalConfigurationValuesByPathValuePairs(
                        ['SYS/systemMaintainers' => $newSystemMaintainersList]
                    );
                }

                $messages->enqueue(new FlashMessage(
                    'An administrator with username "' . $username . '" has been created successfully.',
                    'Administrator created'
                ));
            }
        }
        return new JsonResponse([
            'success' => true,
            'status' => $messages,
            'userCreated' => $userCreated,
        ]);
    }

    /**
     * Entry action of language packs module gets
     * * list of available languages with details like active or not and last update
     * * list of loaded extensions
     */
    public function languagePacksGetDataAction(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->initializeView($request);
        $formProtection = $this->formProtectionFactory->createFromRequest($request);
        $isWritable = $this->configurationManager->canWriteConfiguration();
        $view->assignMultiple([
            'isWritable' => $isWritable,
            'languagePacksActivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksActivateLanguage'),
            'languagePacksDeactivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksDeactivateLanguage'),
            'languagePacksUpdatePackToken' => $formProtection->generateToken('installTool', 'languagePacksUpdatePack'),
            'languagePacksUpdateIsoTimesToken' => $formProtection->generateToken('installTool', 'languagePacksUpdateIsoTimes'),
        ]);
        // This action needs TYPO3_CONF_VARS for full GeneralUtility::getUrl() config
        $container = $this->lateBootService->loadExtLocalconfDatabase(false, true);
        $languagePackService = $container->get(LanguagePackService::class);
        $extensions = $languagePackService->getExtensionLanguagePackDetails();
        $extensionList = array_map(function (array $extension) {
            $extension['packs'] = array_values($extension['packs']);
            return $extension;
        }, array_values($extensions));
        return new JsonResponse([
            'success' => true,
            'languages' => $languagePackService->getLanguageDetails(),
            'extensions' => $extensionList,
            'activeLanguages' => $languagePackService->getActiveLanguages(),
            'activeExtensions' => array_column($extensions, 'key'),
            'html' => $view->render('Maintenance/LanguagePacks'),
        ]);
    }

    /**
     * Activate a language and any possible dependency it may have
     */
    public function languagePacksActivateLanguageAction(ServerRequestInterface $request): ResponseInterface
    {
        $messageQueue = new FlashMessageQueue('install');
        $container = $this->lateBootService->getContainer(true);
        $languagePackService = $container->get(LanguagePackService::class);
        $availableLanguages = $languagePackService->getAvailableLanguages();
        $activeLanguages = $languagePackService->getActiveLanguages();
        $iso = $request->getParsedBody()['install']['iso'];

        if (!$this->configurationManager->canWriteConfiguration()) {
            $messageQueue->enqueue(new FlashMessage(
                sprintf('The language %s was not activated as the configuration file is not writable.', $availableLanguages[$iso]),
                'Language not activated',
                ContextualFeedbackSeverity::ERROR
            ));
        } else {
            $activateArray = [];
            foreach ($availableLanguages as $availableIso => $name) {
                if ($availableIso === $iso && !in_array($availableIso, $activeLanguages, true)) {
                    $activateArray[] = $iso;
                    $dependencies = $this->locales->getLocaleDependencies($availableIso);
                    if (!empty($dependencies)) {
                        foreach ($dependencies as $dependency) {
                            if (!in_array($dependency, $activeLanguages, true)) {
                                $activateArray[] = $dependency;
                            }
                        }
                    }
                }
            }
            if (!empty($activateArray)) {
                $activeLanguages = array_merge($activeLanguages, $activateArray);
                sort($activeLanguages);
                $this->configurationManager->setLocalConfigurationValueByPath(
                    'LANG',
                    ['availableLocales' => $activeLanguages]
                );
                $activationArray = [];
                foreach ($activateArray as $activateIso) {
                    $activationArray[] = $availableLanguages[$activateIso] . ' (' . $activateIso . ')';
                }
                $messageQueue->enqueue(new FlashMessage(
                    'These languages have been activated: ' . implode(', ', $activationArray)
                ));
            } else {
                $messageQueue->enqueue(new FlashMessage(
                    'Language with ISO code "' . $iso . '" not found or already active.',
                    '',
                    ContextualFeedbackSeverity::ERROR
                ));
            }
        }
        return new JsonResponse([
            'success' => true,
            'status' => $messageQueue,
        ]);
    }

    /**
     * Deactivate a language if no other active language depends on it
     *
     * @throws \RuntimeException
     */
    public function languagePacksDeactivateLanguageAction(ServerRequestInterface $request): ResponseInterface
    {
        $messageQueue = new FlashMessageQueue('install');
        $container = $this->lateBootService->getContainer(true);
        $languagePackService = $container->get(LanguagePackService::class);
        $availableLanguages = $languagePackService->getAvailableLanguages();
        $activeLanguages = $languagePackService->getActiveLanguages();
        $iso = $request->getParsedBody()['install']['iso'];

        if (!$this->configurationManager->canWriteConfiguration()) {
            $messageQueue->enqueue(new FlashMessage(
                sprintf('The language %s was not deactivated as the configuration file is not writable.', $availableLanguages[$iso]),
                'Language not deactivated',
                ContextualFeedbackSeverity::ERROR
            ));
        } else {
            if (empty($iso)) {
                throw new \RuntimeException('No iso code given', 1520109807);
            }
            $otherActiveLanguageDependencies = [];
            foreach ($activeLanguages as $activeLanguage) {
                if ($activeLanguage === $iso) {
                    continue;
                }
                $dependencies = $this->locales->getLocaleDependencies($activeLanguage);
                if (in_array($iso, $dependencies, true)) {
                    $otherActiveLanguageDependencies[] = $activeLanguage;
                }
            }
            if (!empty($otherActiveLanguageDependencies)) {
                // Error: Must disable dependencies first
                $dependentArray = [];
                foreach ($otherActiveLanguageDependencies as $dependency) {
                    $dependentArray[] = $availableLanguages[$dependency] . ' (' . $dependency . ')';
                }
                $messageQueue->enqueue(new FlashMessage(
                    'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" can not be deactivated. These'
                    . ' other languages depend on it and need to be deactivated before:'
                    . implode(', ', $dependentArray),
                    '',
                    ContextualFeedbackSeverity::ERROR
                ));
            } else {
                if (in_array($iso, $activeLanguages, true)) {
                    // Deactivate this language
                    $newActiveLanguages = [];
                    foreach ($activeLanguages as $activeLanguage) {
                        if ($activeLanguage === $iso) {
                            continue;
                        }
                        $newActiveLanguages[] = $activeLanguage;
                    }
                    $this->configurationManager->setLocalConfigurationValueByPath(
                        'LANG',
                        ['availableLocales' => $newActiveLanguages]
                    );
                    $messageQueue->enqueue(new FlashMessage(
                        'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has been deactivated'
                    ));
                } else {
                    $messageQueue->enqueue(new FlashMessage(
                        'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has not been deactivated',
                        '',
                        ContextualFeedbackSeverity::ERROR
                    ));
                }
            }
        }
        return new JsonResponse([
            'success' => true,
            'status' => $messageQueue,
        ]);
    }

    /**
     * Update a pack of one extension and one language
     *
     * @throws \RuntimeException
     */
    public function languagePacksUpdatePackAction(ServerRequestInterface $request): ResponseInterface
    {
        $container = $this->lateBootService->loadExtLocalconfDatabase(false, true);
        $iso = $request->getParsedBody()['install']['iso'];
        $key = $request->getParsedBody()['install']['extension'];

        $languagePackService = $container->get(LanguagePackService::class);

        // Gate untrusted user input against the set of extensions and languages exposed for download.
        $extensions = $languagePackService->getExtensionLanguagePackDetails();
        if (!isset($extensions[$key]['packs'][$iso])) {
            return new JsonResponse([
                'success' => true,
                'packResult' => 'skipped',
            ]);
        }

        return new JsonResponse([
            'success' => true,
            'packResult' => $languagePackService->languagePackDownload($key, $iso),
        ]);
    }

    /**
     * Set "last updated" time in registry for fully updated language packs.
     */
    public function languagePacksUpdateIsoTimesAction(ServerRequestInterface $request): ResponseInterface
    {
        $isos = $request->getParsedBody()['install']['isos'];
        $container = $this->lateBootService->getContainer(true);
        $languagePackService = $container->get(LanguagePackService::class);
        $languagePackService->setLastUpdatedIsoCode($isos);

        // The cache manager is already instantiated in the install tool
        // with some hacked settings to disable caching of extbase and fluid.
        // We want a "fresh" object here to operate on a different cache setup.
        // cacheManager implements SingletonInterface, so the only way to get a "fresh"
        // instance is by circumventing makeInstance and using new directly!
        $cacheManager = new CacheManager();
        $cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);
        $cacheManager->getCache('l10n')->flush();

        return new JsonResponse(['success' => true]);
    }

    /**
     * Set 'uc' field of all backend users to empty string
     */
    public function resetBackendUserUcAction(): ResponseInterface
    {
        $container = $this->lateBootService->getContainer(true);
        $connectionPool = $container->get(ConnectionPool::class);
        $connectionPool
            ->getQueryBuilderForTable('be_users')
            ->update('be_users')
            ->set('uc', '')
            ->executeStatement();
        $messageQueue = new FlashMessageQueue('install');
        $messageQueue->enqueue(new FlashMessage(
            'Preferences of all backend users have been reset',
            'Reset preferences of all backend users'
        ));
        return new JsonResponse([
            'success' => true,
            'status' => $messageQueue,
        ]);
    }

    /**
     * Show reference index card
     */
    public function referenceIndexAction(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->initializeView($request);
        $view->assignMultiple([
            'referenceIndexToken' => $this->formProtectionFactory->createFromRequest($request)->generateToken('installTool', 'referenceIndexUpdate'),
            'binaryPath' => ExtensionManagementUtility::extPath('core', 'bin/typo3'),
        ]);
        return new JsonResponse([
            'success' => true,
            'html' => $view->render('Maintenance/ReferenceIndex'),
            'buttons' => [
                [
                    'btnClass' => 'btn-default t3js-referenceIndex-check',
                    'text' => 'Check Reference Index',
                ],
                [
                    'btnClass' => 'btn-default t3js-referenceIndex-update',
                    'text' => 'Update Reference Index',
                ],
            ],
        ]);
    }

    /**
     * Check or update reference index
     */
    public function referenceIndexUpdateAction(ServerRequestInterface $request): ResponseInterface
    {
        $isCheckOnly = (bool)($request->getParsedBody()['install']['checkOnly'] ?? false);

        $container = $this->lateBootService->loadExtLocalconfDatabase(false, true);
        $result = $container->get(ReferenceIndex::class)->updateIndex($isCheckOnly);

        $messageQueue = new FlashMessageQueue('install');
        if (!empty($result['errors'])) {
            foreach ($result['errors'] as $error) {
                $messageQueue->enqueue(new FlashMessage(
                    $error,
                    'Reference Index Issue',
                    ContextualFeedbackSeverity::WARNING
                ));
            }
        } else {
            $messageQueue->enqueue(new FlashMessage(
                $isCheckOnly ? 'Reference index check completed successfully' : 'Reference index has been updated successfully',
                $isCheckOnly ? 'Check Complete' : 'Update Complete'
            ));
        }

        return new JsonResponse([
            'success' => true,
            'status' => $messageQueue,
            'result' => $result,
        ]);
    }
}
