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

use Symfony\Component\Console\Application as SymfonyConsoleApplication;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Adapter\EventDispatcherAdapter as SymfonyEventDispatcher;
use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\DateTimeAspect;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Context\VisibilityAspect;
use TYPO3\CMS\Core\Context\WorkspaceAspect;
use TYPO3\CMS\Core\Core\ApplicationInterface;
use TYPO3\CMS\Core\Core\BootService;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Domain\DateTimeFactory;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

/**
 * Entry point for the TYPO3 Command Line for Commands
 * In addition to a simple Symfony Command, this also sets up a CLI user
 */
class CommandApplication implements ApplicationInterface
{
    protected Context $context;

    protected CommandRegistry $commandRegistry;

    protected ConfigurationManager $configurationManager;

    protected BootService $bootService;

    protected LanguageServiceFactory $languageServiceFactory;

    protected SymfonyConsoleApplication $application;

    public function __construct(
        Context $context,
        CommandRegistry $commandRegistry,
        EventDispatcherInterface $eventDispatcher,
        ConfigurationManager $configurationMananger,
        BootService $bootService,
        LanguageServiceFactory $languageServiceFactory
    ) {
        $this->context = $context;
        $this->commandRegistry = $commandRegistry;
        $this->configurationManager = $configurationMananger;
        $this->bootService = $bootService;
        $this->languageServiceFactory = $languageServiceFactory;

        $this->checkEnvironmentOrDie();
        $this->application = new Application('TYPO3 CMS', (new Typo3Version())->getVersion());
        $this->application->setAutoExit(false);
        $this->application->setDispatcher($eventDispatcher);
        $this->application->setCommandLoader($commandRegistry);
        // Replace default list command with TYPO3 override
        $this->application->addCommands([$commandRegistry->get('list')]);
    }

    /**
     * Run the Symfony Console application in this TYPO3 application
     */
    public function run()
    {
        $input = new ArgvInput();
        $output = new ConsoleOutput();

        $commandName = $this->getCommandName($input);
        if ($this->wantsFullBoot($commandName)) {
            // Do a full container boot if command is not a 1:1 matching low-level command
            $container = $this->bootService->getContainer();
            $eventDispatcher = $container->get(SymfonyEventDispatcher::class);
            $commandRegistry = $container->get(CommandRegistry::class);
            $this->application->setDispatcher($eventDispatcher);
            $this->application->setCommandLoader($commandRegistry);
            $this->context = $container->get(Context::class);

            $realName = $this->resolveShortcut($commandName, $commandRegistry);
            $isLowLevelCommandShortcut = $realName !== null && !$this->wantsFullBoot($realName);
            // Load ext_localconf, except if a low level command shortcut was found
            // or if essential configuration is missing
            if (!$isLowLevelCommandShortcut && Bootstrap::checkIfEssentialConfigurationExists($this->configurationManager)) {
                $this->bootService->loadExtLocalconfDatabase();
            }
        }

        // Make sure output is not buffered, so command-line output and interaction can take place.
        // Bootstrap does not open a buffer anymore, but third-party extension code may have done
        // so while ext_localconf.php files were loaded.
        while (ob_get_level()) {
            ob_end_clean();
        }

        $this->initializeContext();
        // create the BE_USER object (not logged in yet)
        Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
        $GLOBALS['LANG'] = $this->languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);

        $exitCode = $this->application->run($input, $output);
        // exit codes > 255 are not handled in UNIX
        if ($exitCode > 255) {
            $exitCode = 255;
        }

        exit($exitCode);
    }

    private function resolveShortcut(string $commandName, CommandRegistry $commandRegistry): ?string
    {
        if ($commandRegistry->has($commandName)) {
            return $commandName;
        }

        $allCommands = $commandRegistry->getNames();
        $expr = implode('[^:]*:', array_map(preg_quote(...), explode(':', $commandName))) . '[^:]*';
        $commands = preg_grep('{^' . $expr . '}', $allCommands);

        if ($commands === false || count($commands) === 0) {
            $commands = preg_grep('{^' . $expr . '}i', $allCommands);
        }

        if ($commands === false || count($commands) !== 1) {
            return null;
        }

        return reset($commands);
    }

    protected function wantsFullBoot(string $commandName): bool
    {
        if ($commandName === 'help') {
            return true;
        }
        return !$this->commandRegistry->has($commandName);
    }

    protected function getCommandName(ArgvInput $input): string
    {
        try {
            $input->bind($this->application->getDefinition());
        } catch (ExceptionInterface $e) {
            // Errors must be ignored, full binding/validation happens later when the console application runs.
        }

        return $input->getFirstArgument() ?? 'list';
    }

    /**
     * Check the script is called from a cli environment.
     */
    protected function checkEnvironmentOrDie(): void
    {
        if (PHP_SAPI !== 'cli') {
            die('Not called from a command line interface (e.g. a shell or scheduler).' . LF);
        }
    }

    /**
     * Initializes the Context used for accessing data and finding out the current state of the application
     */
    protected function initializeContext(): void
    {
        $this->context->setAspect('date', new DateTimeAspect(DateTimeFactory::createFromTimestamp($GLOBALS['EXEC_TIME'])));
        $this->context->setAspect('visibility', new VisibilityAspect(true, true, false, true));
        $this->context->setAspect('workspace', new WorkspaceAspect(0));
        $this->context->setAspect('backend.user', new UserAspect(null));
    }
}
