<?php
/**
 * i-doit - Documentation and CMDB solution for IT environments
 *
 * This file is part of the i-doit framework. Modify at your own risk.
 *
 * Please visit http://www.i-doit.com/license for a full copyright and license information.
 *
 * @version     1.10
 * @package     i-doit
 * @author      synetics GmbH
 * @copyright   synetics GmbH
 * @url         http://www.i-doit.com
 * @license     http://www.i-doit.com/license
 */

namespace idoit\Console\Command\Search;

use idoit\Console\Command\AbstractCommand;
use idoit\Module\Cmdb\Search\IndexExtension\Config;
use idoit\Module\Cmdb\Search\IndexExtension\Manager\CmdbIndexManager;
use idoit\Module\Cmdb\Search\IndexExtension\OnelineIndexer;
use idoit\Module\Search\Index\Indexer;
use idoit\Module\Search\Index\Observer\Mysql;
use idoit\Module\Search\Index\Observer\ObserverRegistry;
use idoit\Module\Search\Index\Protocol\Document;
use idoit\Module\Search\Index\Protocol\ObservableIndexManager;
use idoit\Module\Search\Index\Protocol\Observer;
use idoit\Module\Search\Index\Registry;
use isys_application;
use isys_cmdb_dao_category;
use isys_tenantsettings;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Helper\ProgressBar;
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\Output\OutputInterface;
use Symfony\Component\Console\Helper;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;

class IndexerCommand extends AbstractCommand implements Observer
{
    const NAME = 'search-index';

    /**
     * @var array
     */
    public static $defaultObjectTypeBlacklist = [
        C__OBJTYPE__CONTAINER,
        C__OBJTYPE__GENERIC_TEMPLATE,
        C__OBJTYPE__PARALLEL_RELATION,
        C__OBJTYPE__RELATION,
        C__OBJTYPE__LOCATION_GENERIC,
        C__OBJTYPE__MIGRATION_OBJECT,
        C__OBJTYPE__NAGIOS_HOST_TPL,
        C__OBJTYPE__NAGIOS_SERVICE_TPL,
        C__OBJTYPE__CABLE
    ];

    /**
     *   Skip indexing of the following categories:
     *
     *   - logbook, relation: these categories should not be searched
     *   - connector: this category is a unification of ports, fc-ports and ui, search results should lead into these categories
     *   - indentifier: this category is for referencing i-doit objects to foreign data sources
     *   - cats application
     *   - person login: because of the password
     *
     * @var array
     */
    public static $defaultCategoryBlacklist = [
        'C__CATG__LOGBOOK',
        'C__CATG__RELATION',
        'C__CATG__CONNECTOR',
        'C__CATG__IDENTIFIER',
        'C__CATS__APPLICATION',
        'C__CATS__PERSON_LOGIN',
    ];

    /**
     * @var array
     */
    public static $defaultCategoryTypeBlacklist = [
        isys_cmdb_dao_category::TYPE_ASSIGN,
        isys_cmdb_dao_category::TYPE_FOLDER,
        isys_cmdb_dao_category::TYPE_REAR,
        isys_cmdb_dao_category::TYPE_VIEW
    ];

    /**
     * @var array
     */
    public static $defaultCategoryTypes = [
        isys_cmdb_dao_category::TYPE_EDIT
    ];

    /**
     * @return array
     */
    public static function defaultCategoryTypes()
    {
        return self::$defaultCategoryTypes;
    }

    /**
     * @return array
     */
    public static function defaultCategoryTypeBlacklist()
    {
        return self::$defaultCategoryTypeBlacklist;
    }

    /**
     * @return array
     */
    public static function initCategoryBlacklist()
    {
        return self::$defaultCategoryBlacklist;
    }

    /**
     * @return array
     */
    public static function defaultObjectTypeBlacklist()
    {
        return [
            C__OBJTYPE__CONTAINER,
            C__OBJTYPE__GENERIC_TEMPLATE,
            C__OBJTYPE__PARALLEL_RELATION,
            C__OBJTYPE__RELATION,
            C__OBJTYPE__LOCATION_GENERIC,
            C__OBJTYPE__MIGRATION_OBJECT,
            C__OBJTYPE__NAGIOS_HOST_TPL,
            C__OBJTYPE__NAGIOS_SERVICE_TPL,
            C__OBJTYPE__CABLE
        ];
    }

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

    /**
     * @var Helper\ProgressBar
     */
    private $progressBar = null;

    /**
     * Get array of category for the full default index
     *
     * @return array
     */
    public static function defaultCategoryBlacklist()
    {
        // Get dao instance
        $cmdbDao = \isys_cmdb_dao::instance(isys_application::instance()->container->database);

        // Initialize default category blacklist
        $categoryBlacklist = self::initCategoryBlacklist();

        // Retrieve all categories of type $categoryTypes, we should only get categories here that should not be indexed!
        $allCategories = $cmdbDao->get_all_categories(self::defaultCategoryTypeBlacklist());

        foreach ([
                     C__CMDB__CATEGORY__TYPE_GLOBAL,
                     C__CMDB__CATEGORY__TYPE_SPECIFIC,
                     C__CMDB__CATEGORY__TYPE_CUSTOM
                 ] as $categoryType)
        {
            foreach ($allCategories[$categoryType] as $l_cat)
            {
                // Blacklist this category as "not-to-be-indexed"
                $categoryBlacklist[] = $l_cat['const'];
            }
        }

        return $categoryBlacklist;
    }

    /**
     * Get name for command
     *
     * @return string
     */
    public function getCommandName()
    {
        return self::NAME;
    }

    /**
     * Get description for command
     *
     * @return string
     */
    public function getCommandDescription()
    {
        return 'Triggers a reindex of the search index';
    }

    /**
     * Retrieve Command InputDefinition
     *
     * @return InputDefinition
     */
    public function getCommandDefinition()
    {
        $definition = new InputDefinition();
        $definition->addOption(new InputOption('full', null, InputOption::VALUE_NONE, 'Do a full index.'));
        $definition->addOption(new InputOption('reindex', null, InputOption::VALUE_NONE, 'Reindex data.'));

        return $definition;
    }

    /**
     * Checks if a command can have a config file via --config
     *
     * @return bool
     */
    public function isConfigurable()
    {
        return true;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if (!$input->getOption('full') && !$input->getOption('reindex')) {
            throw new MissingOptionsException('Please provide either --full or --reindex!');
        }

        $this->setSearchIndexMemoryLimit();

        $this->output = $output;

        $style = new OutputFormatterStyle('red');
        $this->output->getFormatter()->setStyle('red', $style);

        $time_start = microtime(true);

        $observers = [
            // Attach self to retrieve status updates for cli commands
            'indexerCommand' => $this
        ];

        if ($input->getOption('full') && !$input->getOption('reindex')) {
            $this->fullIndex();
        }

        if ($input->getOption('reindex') && !$input->getOption('full')) {
            $this->reIndex();
        }

        $indexer = $this->createIndex($observers);

        // Sum it all up
        $time_end = microtime(true);
        $time     = number_format($time_end - $time_start, 2);

        $this->output->writeln(
            sprintf(
                "Finished indexing in <info>%s</info> seconds. Indexed %s properties from %s objects(s).",
                $time,
                $indexer->getIndexedItems(),
                $indexer->getIndexedDocuments()
            )
        );
    }

    private function fullIndex()
    {
        /**
         * Register cmdb index extension. Used for creating a cmdb index.
         */
        Registry::register(
            'CMDB',
            function (array $observers, array $categoryBlacklist, array $objectTypeBlacklist)
            {
                // Create index manager for cis
                return new CmdbIndexManager(
                    new Config(
                        $objectTypeBlacklist, $categoryBlacklist, [], [
                            C__PROPERTY__PROVIDES__SEARCH
                        ]
                    )
                );
            }
        );
    }

    private function reIndex()
    {
        // Use OnelineIndexer for fast index processing:
        Registry::unregister('CMDB');

        Registry::register(
            'CMDB-Reindex',
            function ()
            {
                return new OnelineIndexer($this->container->database);
            }
        );
    }

    /**
     * Index Creation
     *
     * @param Observer[]  $observers
     * @param string $onlyIndexManagerNamed
     *
     * @return Indexer
     */
    public function createIndex($observers = [], $onlyIndexManagerNamed = '')
    {
        // Create Indexer instance
        $indexer = new Indexer();

        $categoryBlacklist   = self::$defaultCategoryBlacklist;
        $objectTypeBlacklist = self::$defaultObjectTypeBlacklist;

        ObserverRegistry::register(
            'MySQL',
            function ()
            {
                //Attach cmdb observer to create mysql index
                return new Mysql(
                    $this->container, false
                );
            }
        );

        /**
         * Retrieve all observers and merge with defaults
         */
        $observers = array_merge($observers, ObserverRegistry::get() ?: []);

        // Iterate through registered index managers
        foreach (Registry::get($observers, $categoryBlacklist, $objectTypeBlacklist) as $name => $ciIndexManager)
        {
            if ($onlyIndexManagerNamed !== '' && $name !== $onlyIndexManagerNamed)
            {
                continue;
            }

            foreach ($observers as $observerName => $observer)
            {
                $ciIndexManager->attach($observer);
            }

            // Attach index manager to indexer
            $indexer->attachManager($ciIndexManager);
        }

        // Start index creation
        return $indexer->create();
    }

    /**
     * Call update function
     *
     * @param Document               $document
     * @param ObservableIndexManager $indexManager
     *
     * @return $this
     */
    public function update(Document $document, ObservableIndexManager $indexManager)
    {
        if ($this->progressBar === null) {
            $this->progressBar = new Helper\ProgressBar($this->output, $indexManager->getDocumentsToBeIndexed());
            $this->progressBar->setFormatDefinition('default', '');
            $this->progressBar->setFormat( $indexManager->getName() . ': %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'. "\n\r" . 'Indexing document: %message% (%documentId%)' . "\n\r");
            $this->progressBar->start();
        }

        $this->progressBar->setMessage($document->getTitle());
        $this->progressBar->setMessage($document->getId(), 'documentId');

        $this->progressBar->advance($indexManager->getIndexedDocuments() - $this->progressBar->getProgress());

        if ($this->progressBar->getProgress() === $this->progressBar->getMaxSteps()) {
            $this->progressBar->finish();
        }
    }

    /**
     * Returns an array of command usages
     *
     * @return string[]
     */
    public function getCommandUsages()
    {
        return [
            '--full -u {user} -p {password} -i {tenantId}',
            '--reindex -u {user} -p {password} -i {tenantId}'
        ];
    }

    /**
     * Helper method which checks if its necessary to set the memory_limit higher for the search index.
     *
     * @author   Van Quyen Hoang <qhoang@i-doit.com>
     */
    private function setSearchIndexMemoryLimit()
    {
        $configuredMemoryLimit = strtoupper(ini_get('memory_limit'));
        $limit = null;
        $map = ['K' => 1024 * 1024, 'M' => 1024, 'G' => 1];
        foreach($map AS $key => $val)
        {
            if(($pos = stripos($configuredMemoryLimit, $key)) !== false)
            {
                $limit = substr($configuredMemoryLimit, 0, $pos) / $val;
                break;
            } // if
        } // foreach

        if($limit === null || $limit < 4)
        {
            ini_set('memory_limit', isys_tenantsettings::get('system.memory-limit.searchindex', '4G'));
        } // if
    } // function
}
