<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2022 Teclib' and contributors.
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * GLPI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 */

namespace Glpi\Console;

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

use Config;
use DB;
use GLPI;
use Glpi\Application\ErrorHandler;
use Glpi\Console\Command\ForceNoPluginsOptionCommandInterface;
use Glpi\Console\Command\GlpiCommandInterface;
use Glpi\System\RequirementsManager;
use Plugin;
use Session;
use Toolbox;

use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Output\OutputInterface;

class Application extends BaseApplication {

   /**
    * Error code returned when system requirements are missing.
    *
    * @var integer
    */
   const ERROR_MISSING_REQUIREMENTS = 128; // start application codes at 128 be sure to be different from commands codes

   /**
    * Error code returned when DB is not up-to-date.
    *
    * @var integer
    */
   const ERROR_DB_OUTDATED = 129;

   /**
    * Pointer to $CFG_GLPI.
    * @var array
    */
   private $config;

   /**
    * @var ErrorHandler
    */
   private $error_handler;

   /**
    * @var DB
    */
   private $db;

   /**
    * @var OutputInterface
    */
   private $output;

   public function __construct() {

      parent::__construct('GLPI CLI', GLPI_VERSION);

      $this->initApplication();
      $this->initDb();
      $this->initSession();
      $this->initCache();
      $this->initConfig();

      $this->computeAndLoadOutputLang();

      // Load core commands only to check if called command prevent or not usage of plugins
      // Plugin commands will be loaded later
      $loader = new CommandLoader(false);
      $this->setCommandLoader($loader);

      if ($this->usePlugins()) {
         $plugin = new Plugin();
         $plugin->init(true);
         $loader->setIncludePlugins(true);
      }
   }

   protected function getDefaultInputDefinition() {

      $definition = new InputDefinition(
         [
            new InputArgument(
               'command',
               InputArgument::REQUIRED,
               __('The command to execute')
            ),

            new InputOption(
               '--help',
               '-h',
               InputOption::VALUE_NONE,
               __('Display this help message')
            ),
            new InputOption(
               '--quiet',
               '-q',
               InputOption::VALUE_NONE,
               __('Do not output any message')
            ),
            new InputOption(
               '--verbose',
               '-v|vv|vvv',
               InputOption::VALUE_NONE,
               __('Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug')
            ),
            new InputOption(
               '--version',
               '-V',
               InputOption::VALUE_NONE,
               __('Display this application version')
            ),
            new InputOption(
               '--ansi',
               null,
               InputOption::VALUE_NONE,
               __('Force ANSI output')
            ),
            new InputOption(
               '--no-ansi',
               null,
               InputOption::VALUE_NONE,
               __('Disable ANSI output')
            ),
            new InputOption(
               '--no-interaction',
               '-n',
               InputOption::VALUE_NONE,
               __('Do not ask any interactive question')
            ),
            new InputOption(
               '--config-dir',
               null,
               InputOption::VALUE_OPTIONAL,
               __('Configuration directory to use')
            ),
            new InputOption(
               '--no-plugins',
               null,
               InputOption::VALUE_NONE,
               __('Disable GLPI plugins (unless commands forces plugins loading)')
            ),
            new InputOption(
               '--lang',
               null,
               InputOption::VALUE_OPTIONAL,
               __('Output language (default value is existing GLPI "language" configuration or "en_GB")')
            )
         ]
      );

      return $definition;
   }

   protected function configureIO(InputInterface $input, OutputInterface $output) {

      global $CFG_GLPI;

      $this->output = $output;
      $this->error_handler->setOutputHandler($output);

      parent::configureIO($input, $output);

      // Trigger error on invalid lang. This is not done before as error handler would not be set.
      $lang = $input->getParameterOption('--lang', null, true);
      if (null !== $lang && !array_key_exists($lang, $CFG_GLPI['languages'])) {
         throw new RuntimeException(
            sprintf(__('Invalid "--lang" option value "%s".'), $lang)
         );
      }

      if ($output->getVerbosity() === OutputInterface::VERBOSITY_DEBUG) {
         Toolbox::setDebugMode(Session::DEBUG_MODE, 0, 0, 1);
      }
   }

   /**
    * Returns output handler.
    *
    * @return OutputInterface
    */
   public function getOutput() {
      return $this->output;
   }

   protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) {

      $begin_time = microtime(true);

      if ($command instanceof GlpiCommandInterface && $command->requiresUpToDateDb()
          && (!array_key_exists('dbversion', $this->config) || (trim($this->config['dbversion']) != GLPI_SCHEMA_VERSION))) {
         $output->writeln(
            '<error>'
            . __('The version of the database is not compatible with the version of the installed files. An update is necessary.')
            . '</error>'
         );
         return self::ERROR_DB_OUTDATED;
      }

      if ($command instanceof GlpiCommandInterface && $command->mustCheckMandatoryRequirements()
          && !$this->checkCoreMandatoryRequirements()) {
         return self::ERROR_MISSING_REQUIREMENTS;
      }

      $result = parent::doRunCommand($command, $input, $output);

      if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE) {
         $output->writeln(
            sprintf(
               __('Time elapsed: %s.'),
               Helper::formatTime(microtime(true) - $begin_time)
            )
         );
         $output->writeln(
            sprintf(
               __('Memory usage: %s.'),
               Helper::formatMemory(memory_get_peak_usage(true))
            )
         );
      }

      return $result;
   }

   /**
    * Initalize GLPI.
    *
    * @global array $CFG_GLPI
    * @global GLPI  $GLPI
    *
    * @return void
    */
   private function initApplication() {

      // Disable debug at bootstrap (will be re-enabled later if requested by verbosity level).
      global $CFG_GLPI;
      $CFG_GLPI = array_merge(
         $CFG_GLPI,
         [
            'debug_sql'  => 0,
            'debug_vars' => 0,
         ]
      );

      global $GLPI;
      $GLPI = new GLPI();
      $GLPI->initLogger();
      $this->error_handler = $GLPI->initErrorHandler();

      Config::detectRootDoc();
   }

   /**
    * Initialize database connection.
    *
    * @global DB $DB
    *
    * @return void
    *
    * @throws RuntimeException
    */
   private function initDb() {

      if (!class_exists('DB', false) || !class_exists('mysqli', false)) {
         return;
      }

      global $DB;
      $DB = new DB();
      $this->db = $DB;

      if (!$this->db->connected) {
         return;
      }

      ob_start();
      $checkdb = Config::displayCheckDbEngine();
      $message = ob_get_clean();
      if ($checkdb > 0) {
         throw new RuntimeException($message);
      }
   }

   /**
    * Initialize GLPI session.
    * This is mandatory to init cache and load languages.
    *
    * @TODO Do not use session for console.
    *
    * @return void
    */
   private function initSession() {

      if (!is_writable(GLPI_SESSION_DIR)) {
         throw new RuntimeException(
            sprintf(__('Cannot write in "%s" directory.'), GLPI_SESSION_DIR)
         );
      }

      Session::setPath();
      Session::start();

      // Default value for use mode
      $_SESSION['glpi_use_mode'] = Session::NORMAL_MODE;
      $_SESSION['glpiname'] = 'cli';
   }

   /**
    * Initialize GLPI cache.
    *
    * @global Laminas\Cache\Storage\StorageInterface $GLPI_CACHE
    *
    * @return void
    */
   private function initCache() {

      global $GLPI_CACHE;
      $GLPI_CACHE = Config::getCache('cache_db');
   }

   /**
    * Initialize GLPI configuration.
    *
    * @global array $CFG_GLPI
    *
    * @return void
    */
   private function initConfig() {

      global $CFG_GLPI;
      $this->config = &$CFG_GLPI;

      if (!($this->db instanceof DB) || !$this->db->connected) {
         return;
      }

      Config::loadLegacyConfiguration(false);
   }

   /**
    * Compute and load output language.
    *
    * @return void
    *
    * @throws RuntimeException
    */
   private function computeAndLoadOutputLang() {

      // 1. Check in command line arguments
      $input = new ArgvInput();
      $lang = $input->getParameterOption('--lang', null, true);

      if (null !== $lang && !$this->isLanguageValid($lang)) {
         // Unset requested lang if invalid
         $lang = null;
      }

      // 2. Check in GLPI configuration
      if (null === $lang && array_key_exists('language', $this->config)
          && $this->isLanguageValid($this->config['language'])) {
         $lang = $this->config['language'];
      }

      // 3. Use default value
      if (null === $lang) {
         $lang = 'en_GB';
      }

      $_SESSION['glpilanguage'] = $lang;

      Session::loadLanguage('', $this->usePlugins());
   }

   /**
    * Check if a language is valid.
    *
    * @param string $language
    *
    * @return boolean
    */
   private function isLanguageValid($language) {
      return is_array($this->config)
         && array_key_exists('languages', $this->config)
         && array_key_exists($language, $this->config['languages']);
   }

   /**
    * Whether or not plugins have to be used.
    *
    * @return boolean
    */
   private function usePlugins() {
      if (!($this->db instanceof DB) || !$this->db->connected) {
         return false;
      }

      $input = new ArgvInput();

      try {
         $command = $this->find($this->getCommandName($input));
         if ($command instanceof ForceNoPluginsOptionCommandInterface) {
            return !$command->getNoPluginsOptionValue();
         }
      } catch (CommandNotFoundException $e) {
         // Command will not be found at this point if it is a plugin command
         $command = null; // Say hello to CS checker
      }

      return !$input->hasParameterOption('--no-plugins', true);
   }

   /**
    * Check if core mandatory requirements are OK.
    *
    * @return boolean  true if requirements are OK, false otherwise
    */
   private function checkCoreMandatoryRequirements(): bool {
      $db = property_exists($this, 'db') ? $this->db : null;

      $requirements_manager = new RequirementsManager();
      $core_requirements = $requirements_manager->getCoreRequirementList(
         $db instanceof \DBmysql && $db->connected ? $db : null
      );

      if ($core_requirements->hasMissingMandatoryRequirements()) {
         $message = __('Some mandatory system requirements are missing.')
            . ' '
            . __('Run "php bin/console glpi:system:check_requirements" for more details.');
         $this->output->writeln(
            '<error>' . $message . '</error>',
            OutputInterface::VERBOSITY_QUIET
         );
         return false;
      }

      return true;
   }
}
