<?php

declare(strict_types=1);

namespace Drush\Runtime;

use Symfony\Component\Console\Output\ConsoleOutput;
use Drush\Application;
use Drush\Commands\DrushCommands;
use Drush\Drush;
use Drush\Preflight\Preflight;

/**
 * Control the Drush runtime environment
 *
 * - Preflight
 * - Symfony application run
 * - Bootstrap
 * - Command execution
 * - Termination
 */
class Runtime
{
    const DRUSH_RUNTIME_COMPLETED_NAMESPACE = 'runtime.execution.completed';
    const DRUSH_RUNTIME_EXIT_CODE_NAMESPACE = 'runtime.exit_code';

    public function __construct(protected Preflight $preflight, protected DependencyInjection $di)
    {
    }

    /**
     * Run the application, catching any errors that may be thrown.
     * Typically, this will happen only for code that fails fast during
     * preflight. Later code should catch and handle its own exceptions.
     */
    public function run($argv): int
    {
        try {
            $output = new ConsoleOutput();
            $status = $this->doRun($argv, $output);
        } catch (\Exception $e) {
            // Fallback to status 1 if the Exception has not indicated otherwise.
            $status = $e->getCode() ?: DrushCommands::EXIT_FAILURE;
            $message = $e->getMessage();
            // Uncaught exceptions could happen early, before our logger
            // and other classes are initialized. Print them and exit.
            $this->preflight->logger()->setDebug(true)->log($message);
        }
        return $status;
    }

    /**
     * Start up Drush
     */
    protected function doRun($argv, $output): int
    {
        // Do the preflight steps
        [$preflightDidRedispatch, $exitStatus] = $this->preflight->preflight($argv);

        // If preflight signals that we are done, then exit early.
        if ($preflightDidRedispatch) {
            return $exitStatus;
        }

        $commandfileSearchpath = $this->preflight->getCommandFilePaths();
        $this->preflight->logger()->log('Commandfile search paths: ' . implode(',', $commandfileSearchpath));
        $this->preflight->config()->set('runtime.commandfile.paths', $commandfileSearchpath);

        // Load the Symfony compatability layer autoloader
        $this->preflight->loadSymfonyCompatabilityAutoloader();

        // Create the Symfony Application et. al.
        $input = $this->preflight->createInput();
        $application = new Application('Drush Commandline Tool', Drush::sanitizeVersionString(Drush::getVersion()));

        // Set up the DI container.
        $container = $this->di->initContainer(
            $application,
            $this->preflight->config(),
            $input,
            $output,
            $this->preflight->environment()->loader(),
            $this->preflight->drupalFinder(),
            $this->preflight->aliasManager()
        );

        // Our termination handlers are set up via dependency injection,
        // as they require classes that are set up in the DI container.
        // We therefore cannot configure them any earlier than this.
        $this->di->installHandlers($container);

        // Now that the DI container has been set up, the Application object will
        // have a reference to the bootstrap manager et. al., so we may use it
        // as needed. Tell the application to coordinate between the Bootstrap
        // manager and the alias manager to select a more specific URI, if
        // one was not explicitly provided earlier in the preflight.
        $application->refineUriSelection($this->preflight->environment()->cwd());

        // Add global options and copy their values into Config.
        $application->configureGlobalOptions();

        // Configure the application object and register all of the commandfiles
        // from the search paths we found above.  After this point, the input
        // and output objects are ready & we can start using the logger, etc.
        $application->configureAndRegisterCommands($input, $output, $commandfileSearchpath, $this->preflight->environment()->loader());

        // Run the Symfony Application
        // Predispatch: call a remote Drush command if applicable (via a 'pre-init' hook)
        // Bootstrap: bootstrap site to the level requested by the command (via a 'post-init' hook)
        $status = $application->run($input, $output);

        // Placate the Drush shutdown handler.
        Runtime::setCompleted();
        Runtime::setExitCode($status);

        return $status;
    }

    /**
     * Mark the current request as having completed successfully.
     */
    public static function setCompleted(): void
    {
        Drush::config()->set(self::DRUSH_RUNTIME_COMPLETED_NAMESPACE, true);
    }

    /**
     * Mark the exit code for current request.
     *
     * @deprecated
     *   Was used by backend.inc
     */
    public static function setExitCode(int $code): void
    {
        Drush::config()->set(self::DRUSH_RUNTIME_EXIT_CODE_NAMESPACE, $code);
    }

    /**
     * Get the exit code for current request.
     *
     * @deprecated
     *   Was used by backend.inc
     */
    public static function exitCode()
    {
        return Drush::config()->get(self::DRUSH_RUNTIME_EXIT_CODE_NAMESPACE, 0);
    }
}
