<?php

/*
 * 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\Report;

use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Service\UpgradeWizardsService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Service\CoreVersionService;
use TYPO3\CMS\Install\Service\Exception\RemoteFetchException;
use TYPO3\CMS\Reports\Status;
use TYPO3\CMS\Reports\StatusProviderInterface;

/**
 * Provides an installation status report.
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
 */
final readonly class InstallStatusReport implements StatusProviderInterface
{
    private const int WRAP_FLAT = 1;
    private const int WRAP_NESTED = 2;

    public function __construct(
        private UpgradeWizardsService $upgradeWizardsService,
        private UriBuilder $uriBuilder,
    ) {}

    /**
     * Compiles a collection of system status checks as a status report.
     *
     * @return Status[]
     */
    public function getStatus(): array
    {
        return [
            'FileSystem' => $this->getFileSystemStatus(),
            'RemainingUpdates' => $this->getRemainingUpdatesStatus(),
            'NewVersion' => $this->getNewVersionStatus(),
        ];
    }

    public function getLabel(): string
    {
        return 'typo3';
    }

    /**
     * Checks for several directories being writable.
     *
     * @return Status Indicates status of the file system
     */
    private function getFileSystemStatus(): Status
    {
        $languageService = $this->getLanguageService();
        $value = $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_writable');
        $message = '';
        $severity = ContextualFeedbackSeverity::OK;
        // Requirement level
        // -1 = not required, but if it exists may be writable or not
        //  0 = not required, if it exists the dir should be writable
        //  1 = required, doesn't have to be writable
        //  2 = required, has to be writable
        $varPath = Environment::getVarPath();
        $sitePath = Environment::getPublicPath();
        $rootPath = Environment::getProjectPath();
        $checkWritable = [
            $sitePath . '/typo3temp/' => 2,
            $sitePath . '/typo3temp/assets/' => 2,
            // only needed when GraphicalFunctions is used
            $sitePath . '/typo3temp/assets/images/' => 0,
            // used in PageGenerator (inlineStyle2Temp) and Backend + Language JS files
            $sitePath . '/typo3temp/assets/css/' => 2,
            $sitePath . '/typo3temp/assets/js/' => 2,
            // fallback storage of FAL
            $sitePath . '/typo3temp/assets/_processed_/' => 0,
            $varPath => 2,
            $varPath . '/transient/' => 2,
            $varPath . '/lock/' => 2,
            Environment::getLabelsPath() => 0,
            $sitePath . '/' . $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] => -1,
            $sitePath . '/' . $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] . '_temp_/' => 0,
        ];

        // Check for writable extension folder files in non-composer mode only
        if (!Environment::isComposerMode()) {
            $checkWritable[Environment::getExtensionsPath()] = 0;
            $checkWritable[$sitePath . '/typo3conf/'] = 2;

        }

        foreach ($checkWritable as $path => $requirementLevel) {
            $relPath = substr($path, strlen($rootPath) + 1);
            if (!@is_dir($path)) {
                // If the directory is missing, try to create it
                GeneralUtility::mkdir($path);
            }
            if (!@is_dir($path)) {
                if ($requirementLevel > 0) {
                    // directory is required
                    $value = $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_missingDirectory');
                    $message .= sprintf($languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_directoryDoesNotExistCouldNotCreate'), $relPath) . '<br />';
                    $severity = ContextualFeedbackSeverity::ERROR;
                } else {
                    $message .= sprintf($languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_directoryDoesNotExist'), $relPath);
                    if ($requirementLevel == 0) {
                        $message .= ' ' . $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_directoryShouldAlsoBeWritable');
                    }
                    $message .= '<br />';
                    if ($severity->value < ContextualFeedbackSeverity::WARNING->value) {
                        $value = $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_nonExistingDirectory');
                        $severity = ContextualFeedbackSeverity::WARNING;
                    }
                }
            } else {
                if (!is_writable($path)) {
                    switch ($requirementLevel) {
                        case 0:
                            $message .= sprintf(
                                $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_directoryShouldBeWritable'),
                                $path
                            ) . '<br />';
                            if ($severity->value < ContextualFeedbackSeverity::WARNING->value) {
                                $value = $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_recommendedWritableDirectory');
                                $severity = ContextualFeedbackSeverity::WARNING;
                            }
                            break;
                        case 2:
                            $value = $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_requiredWritableDirectory');
                            $message .= sprintf(
                                $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_directoryMustBeWritable'),
                                $path
                            ) . '<br />';
                            $severity = ContextualFeedbackSeverity::ERROR;
                            break;
                        default:
                    }
                }
            }
        }
        return new Status($languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_fileSystem'), $value, $message, $severity);
    }

    /**
     * Returns all incomplete update wizards.
     *
     * Fetches all wizards that are not marked "done" in the registry and filters out
     * the ones that should not be rendered (= no upgrade required).
     */
    private function getIncompleteWizards(): array
    {
        $incompleteWizards = $this->upgradeWizardsService->getUpgradeWizardsList();
        $incompleteWizards = array_filter(
            $incompleteWizards,
            static function ($wizard) {
                return $wizard['shouldRenderWizard'];
            }
        );
        return $incompleteWizards;
    }

    /**
     * Checks if there are still updates to perform
     *
     * @return Status Represents whether the installation is completely updated yet
     */
    private function getRemainingUpdatesStatus(): Status
    {
        $languageService = $this->getLanguageService();
        $value = $languageService->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_updateComplete');
        $message = '';
        $severity = ContextualFeedbackSeverity::OK;
        // check if there are update wizards left to perform
        $incompleteWizards = $this->getIncompleteWizards();
        if (count($incompleteWizards)) {
            // At least one incomplete wizard was found
            $value = $languageService->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_updateIncomplete');
            $severity = ContextualFeedbackSeverity::WARNING;
            $url = (string)$this->uriBuilder->buildUriFromRoute('system_upgrade');
            $message = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.install_update'), '<a href="' . htmlspecialchars($url) . '">', '</a>');
        }

        return new Status($languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_remainingUpdates'), $value, $message, $severity);
    }

    /**
     * Checks if there is a new minor TYPO3 version to update to.
     *
     * @return Status Represents whether there is a new version available online
     */
    private function getNewVersionStatus(): Status
    {
        $typoVersion = GeneralUtility::makeInstance(Typo3Version::class);
        $languageService = $this->getLanguageService();
        $coreVersionService = GeneralUtility::makeInstance(CoreVersionService::class);

        // No updates for development versions
        if (!$coreVersionService->isInstalledVersionAReleasedVersion()) {
            return new Status('TYPO3', $typoVersion->getVersion(), $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_isDevelopmentVersion'), ContextualFeedbackSeverity::NOTICE);
        }

        try {
            $versionMaintenanceWindow = $coreVersionService->getMaintenanceWindow();
        } catch (RemoteFetchException $remoteFetchException) {
            return new Status(
                'TYPO3',
                $typoVersion->getVersion(),
                $languageService->sL(
                    'LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_remoteFetchException'
                ),
                ContextualFeedbackSeverity::NOTICE
            );
        }

        if (!$versionMaintenanceWindow->isSupportedByCommunity() && !$versionMaintenanceWindow->isSupportedByElts()) {
            // Version is not maintained
            $message = $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_versionOutdated');
            $status = ContextualFeedbackSeverity::ERROR;
        } else {
            $message = '';
            $status = ContextualFeedbackSeverity::OK;

            // There is an update available
            $availableReleases = [];
            $latestRelease = $coreVersionService->getYoungestPatchRelease();
            $isCurrentVersionElts = $coreVersionService->isCurrentInstalledVersionElts();

            if ($coreVersionService->isPatchReleaseSuitableForUpdate($latestRelease)) {
                $availableReleases[] = $latestRelease;
            }

            if (!$versionMaintenanceWindow->isSupportedByCommunity()) {
                if ($latestRelease->isElts()) {
                    $latestCommunityDrivenRelease = $coreVersionService->getYoungestCommunityPatchRelease();
                    if ($coreVersionService->isPatchReleaseSuitableForUpdate($latestCommunityDrivenRelease)) {
                        $availableReleases[] = $latestCommunityDrivenRelease;
                    }
                } elseif (!$isCurrentVersionElts) {
                    // Inform user about ELTS being available soon if:
                    // - regular support ran out
                    // - the current installed version is no ELTS
                    // - no ELTS update was released, yet
                    $message = sprintf(
                        $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_elts_information'),
                        $typoVersion->getVersion(),
                        '<a href="https://typo3.com/elts" target="_blank" rel="noopener">https://typo3.com/elts</a>'
                    );
                    $status = ContextualFeedbackSeverity::WARNING;
                }
            }

            if ($availableReleases !== []) {
                $messages = [];
                $status = ContextualFeedbackSeverity::WARNING;
                foreach ($availableReleases as $availableRelease) {
                    $versionString = $availableRelease->getVersion();
                    if ($availableRelease->isElts()) {
                        $versionString .= ' ELTS';
                    }
                    if ($coreVersionService->isUpdateSecurityRelevant($availableRelease)) {
                        $status = ContextualFeedbackSeverity::ERROR;
                        $updateMessage = sprintf($languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_newVersionSecurityRelevant'), $versionString);
                    } else {
                        $updateMessage = sprintf($languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_newVersion'), $versionString);
                    }

                    if ($availableRelease->isElts()) {
                        if ($isCurrentVersionElts) {
                            $updateMessage .= ' ' . sprintf(
                                $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_elts_download'),
                                '<a href="https://my.typo3.org" target="_blank" rel="noopener">my.typo3.org</a>'
                            );
                        } else {
                            $updateMessage .= ' ' . sprintf(
                                $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_elts_subscribe'),
                                $coreVersionService->getInstalledVersion(),
                                '<a href="https://typo3.com/elts" target="_blank" rel="noopener">https://typo3.com/elts</a>'
                            );
                        }
                    }
                    $messages[] = $updateMessage;
                }
                $message = $this->wrapList($messages, count($messages) > 1 ? self::WRAP_NESTED : self::WRAP_FLAT);
            }
        }

        return new Status('TYPO3', $typoVersion->getVersion(), $message, $status);
    }

    private function wrapList(array $items, int $style): string
    {
        if ($style === self::WRAP_NESTED) {
            return sprintf(
                '<ul>%s</ul>',
                implode('', $this->wrapItems($items, '<li>', '</li>'))
            );
        }
        return sprintf(
            '<p>%s</p>',
            implode('', $this->wrapItems($items, '<br>', ''))
        );
    }

    private function wrapItems(array $items, string $before, string $after): array
    {
        return array_map(
            static function (string $item) use ($before, $after): string {
                return $before . $item . $after;
            },
            array_filter($items)
        );
    }

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