<?php

namespace Drush\Boot;

use Consolidation\AnnotatedCommand\AnnotationData;
use Drush\Log\DrushLog;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Drupal\Core\DrupalKernel;
use Drush\Drush;
use Drush\Drupal\DrushServiceModifier;

use Drush\Log\LogLevel;

class DrupalBoot8 extends DrupalBoot implements AutoloaderAwareInterface
{
    use AutoloaderAwareTrait;

    /**
     * @var \Drupal\Core\DrupalKernelInterface
     */
    protected $kernel;

    /**
     * @var \Symfony\Component\HttpFoundation\Request
     */
    protected $request;

    /**
     * @return \Symfony\Component\HttpFoundation\Request
     */
    public function getRequest()
    {
        return $this->request;
    }

    /**
     * @param \Symfony\Component\HttpFoundation\Request $request
     */
    public function setRequest($request)
    {
        $this->request = $request;
    }

    /**
     * @return \Drupal\Core\DrupalKernelInterface
     */
    public function getKernel()
    {
        return $this->kernel;
    }

    public function validRoot($path)
    {
        if (!empty($path) && is_dir($path) && file_exists($path . '/autoload.php')) {
            // Additional check for the presence of core/composer.json to
            // grant it is not a Drupal 7 site with a base folder named "core".
            $candidate = 'core/includes/common.inc';
            if (file_exists($path . '/' . $candidate) && file_exists($path . '/core/core.services.yml')) {
                if (file_exists($path . '/core/misc/drupal.js') || file_exists($path . '/core/assets/js/drupal.js')) {
                    return $candidate;
                }
            }
        }
    }

    public function getVersion($drupal_root)
    {
        // Are the class constants available?
        if (!$this->hasAutoloader()) {
            throw new \Exception('Cannot access Drupal 8 class constants - Drupal autoloader not loaded yet.');
        }
        // Drush depends on bootstrap being loaded at this point.
        require_once $drupal_root .'/core/includes/bootstrap.inc';
        if (defined('\Drupal::VERSION')) {
            return \Drupal::VERSION;
        }
    }

    public function confPath($require_settings = true, $reset = false)
    {

        if (\Drupal::hasService('kernel')) {
            $site_path = \Drupal::service('kernel')->getSitePath();
        }
        if (!isset($site_path) || empty($site_path)) {
            $site_path = DrupalKernel::findSitePath($this->getRequest(), $require_settings);
        }
        return $site_path;
    }

    public function addLogger()
    {
        // Provide a logger which sends
        // output to drush_log(). This should catch every message logged through every
        // channel.
        $container = \Drupal::getContainer();
        $parser = $container->get('logger.log_message_parser');

        $drushLogger = Drush::logger();
        $logger = new DrushLog($parser, $drushLogger);
        $container->get('logger.factory')->addLogger($logger);
    }

    public function bootstrapDrupalCore($drupal_root)
    {
        $core = DRUPAL_ROOT . '/core';

        return $core;
    }

    public function bootstrapDrupalSiteValidate()
    {
        parent::bootstrapDrupalSiteValidate();

        // Normalize URI.
        $uri = rtrim($this->uri, '/') . '/';
        $parsed_url = parse_url($uri);

        // Account for users who omit the http:// prefix.
        if (empty($parsed_url['scheme'])) {
            $this->uri = 'http://' . $this->uri;
            $parsed_url = parse_url($this->uri);
        }

        $server = [
            'SCRIPT_FILENAME' => getcwd() . '/index.php',
            'SCRIPT_NAME' => isset($parsed_url['path']) ? $parsed_url['path'] . 'index.php' : '/index.php',
        ];
        $request = Request::create($this->uri, 'GET', [], [], [], $server);
        $this->setRequest($request);
        $confPath = drush_bootstrap_value('confPath', $this->confPath(true, true));
        drush_bootstrap_value('site', $request->getHttpHost());
        return true;
    }

    public function bootstrapDrupalConfigurationValidate()
    {
        $conf_file = $this->confPath() . '/settings.php';
        if (!file_exists($conf_file)) {
            $msg = dt("Could not find a Drupal settings.php file at !file.", ['!file' => $conf_file]);
            $this->logger->debug($msg);
            // Cant do this because site:install deliberately bootstraps to configure without a settings.php file.
            // return drush_set_error($msg);
        }
        return true;
    }

    public function bootstrapDrupalDatabaseValidate()
    {
        return parent::bootstrapDrupalDatabaseValidate() && $this->bootstrapDrupalDatabaseHasTable('key_value');
    }

    public function bootstrapDrupalDatabase()
    {
        // D8 omits this bootstrap level as nothing special needs to be done.
        parent::bootstrapDrupalDatabase();
    }

    public function bootstrapDrupalConfiguration(AnnotationData $annotationData = null)
    {
        // Default to the standard kernel.
        $kernel = Kernels::DRUPAL;
        if (!empty($annotationData)) {
            $kernel = $annotationData->get('kernel', Kernels::DRUPAL);
        }
        $classloader = $this->autoloader();
        $request = $this->getRequest();
        $kernel_factory = Kernels::getKernelFactory($kernel);
        $allow_dumping = $kernel !== Kernels::UPDATE;
        /** @var \Drupal\Core\DrupalKernelInterface kernel */
        $this->kernel = $kernel_factory($request, $classloader, 'prod', $allow_dumping);
        // Include Drush services in the container.
        // @see Drush\Drupal\DrupalKernel::addServiceModifier()
        $this->kernel->addServiceModifier(new DrushServiceModifier());

        // Unset drupal error handler and restore Drush's one.
        restore_error_handler();

        // Disable automated cron if the module is enabled.
        $GLOBALS['config']['automated_cron.settings']['interval'] = 0;

        parent::bootstrapDrupalConfiguration();
    }

    public function bootstrapDrupalFull()
    {
        $this->logger->debug(dt('Start bootstrap of the Drupal Kernel.'));
        $this->kernel->boot();
        $this->kernel->prepareLegacyRequest($this->getRequest());
        $this->logger->debug(dt('Finished bootstrap of the Drupal Kernel.'));

        parent::bootstrapDrupalFull();
        $this->addLogger();

        // Get a list of the modules to ignore
        $ignored_modules = drush_get_option_list('ignored-modules', []);

        $application = Drush::getApplication();
        $runner = Drush::runner();

        // We have to get the service command list from the container, because
        // it is constructed in an indirect way during the container initialization.
        // The upshot is that the list of console commands is not available
        // until after $kernel->boot() is called.
        $container = \Drupal::getContainer();

        // Set the command info alterers.
        if ($container->has(DrushServiceModifier::DRUSH_COMMAND_INFO_ALTERER_SERVICES)) {
            $serviceCommandInfoAltererlist = $container->get(DrushServiceModifier::DRUSH_COMMAND_INFO_ALTERER_SERVICES);
            $commandFactory = Drush::commandFactory();
            foreach ($serviceCommandInfoAltererlist->getCommandList() as $altererHandler) {
                $commandFactory->addCommandInfoAlterer($altererHandler);
                $this->logger->debug(dt('Commands are potentially altered in !class.', ['!class' => get_class($altererHandler)]));
            }
        }

        $serviceCommandlist = $container->get(DrushServiceModifier::DRUSH_CONSOLE_SERVICES);
        if ($container->has(DrushServiceModifier::DRUSH_CONSOLE_SERVICES)) {
            foreach ($serviceCommandlist->getCommandList() as $command) {
                if (!$this->commandIgnored($command, $ignored_modules)) {
                    $this->inflect($command);
                    $this->logger->log(LogLevel::DEBUG_NOTIFY, dt('Add a command: !name', ['!name' => $command->getName()]));
                    $application->add($command);
                }
            }
        }
        // Do the same thing with the annotation commands.
        if ($container->has(DrushServiceModifier::DRUSH_COMMAND_SERVICES)) {
            $serviceCommandlist = $container->get(DrushServiceModifier::DRUSH_COMMAND_SERVICES);
            foreach ($serviceCommandlist->getCommandList() as $commandHandler) {
                if (!$this->commandIgnored($commandHandler, $ignored_modules)) {
                    $this->inflect($commandHandler);
                    $this->logger->log(LogLevel::DEBUG_NOTIFY, dt('Add a commandfile class: !name', ['!name' => get_class($commandHandler)]));
                    $runner->registerCommandClass($application, $commandHandler);
                }
            }
        }
    }

    public function commandIgnored($command, $ignored_modules)
    {
        if (empty($ignored_modules)) {
            return false;
        }
        $ignored_regex = '#\\\\(' . implode('|', $ignored_modules) . ')\\\\#';
        $class = new \ReflectionClass($command);
        $commandNamespace = $class->getNamespaceName();
        return preg_match($ignored_regex, $commandNamespace);
    }

    /**
     * {@inheritdoc}
     */
    public function terminate()
    {
        parent::terminate();

        if ($this->kernel) {
            $response = Response::create('');
            $this->kernel->terminate($this->getRequest(), $response);
        }
    }
}
