<?php

namespace idoit\Module\JDisc\Console\Command;

use Exception;
use idoit\Component\Logger;
use idoit\Console\Command\AbstractCommand;
use idoit\Context\Context;
use idoit\Module\Cmdb\Search\Index\Signals as SearchIndexSignals;
use isys_application;
use isys_array;
use isys_cmdb_dao;
use isys_cmdb_dao_category_g_cloud_subscriptions;
use isys_cmdb_dao_jdisc;
use isys_import_handler_cmdb;
use isys_jdisc_dao_devices;
use isys_jdisc_dao_import;
use isys_jdisc_dao_software;
use isys_module_jdisc;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class JDiscImportCommand extends AbstractCommand
{
    const NAME = 'import-jdisc';
    const LOW_LOG_LEVEL = 1;
    const NORMAL_LOG_LEVEL = 2;
    const HIGH_LOG_LEVEL = 3;

    /**
     * Log instance.
     *
     * @var  Logger
     */
    protected $log = null;

    /**
     * @param InputInterface  $input
     * @param OutputInterface $output
     *
     * @return int|void|null
     * @throws Exception
     * @todo NEEDS REFACTORING!
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        Context::instance()->setOrigin(Context::ORIGIN_CONSOLE);

        // Start logging.
        if ($input->getOption('detailedLogging')) {
            $logLvl = (int) $input->getOption('detailedLogging');
            switch ($logLvl) {
                case self::NORMAL_LOG_LEVEL:
                    $log_level = Logger::INFO;
                    break;
                case self::HIGH_LOG_LEVEL:
                    $log_level = Logger::DEBUG;
                    break;
                case self::LOW_LOG_LEVEL:
                    $log_level = Logger::ERROR;
                    break;
                default:
                    break;
            }
        }

        $log_level = Logger::INFO;
        if ($input->getOption('detailedLogging')) {
            $log_level = Logger::DEBUG;
        }

        // Start logging.
        $this->log = Logger::factory('Import JDisc', BASE_DIR . '/log/jdisc-import.log', $log_level);

        // JDisc module
        $l_jdisc = isys_module_jdisc::factory();

        if ($input->getOption('listProfiles')) {
            $this->listProfiles($l_jdisc->get_jdisc_profiles(), $output);

            return Command::SUCCESS;
        }

        $l_import_counter = 0;

        if (!$input->getOption('profile')) {
            $output->writeln('<error>Please provide the jdisc profile!</error>');
            return Command::FAILURE;
        }

        // Retrieving the profile.
        if ($input->getOption('profile')) {
            $l_profile = $input->getOption('profile');
        }

        // Retrieving the jdisc server.
        if ($input->getOption('server')) {
            $l_jdisc_server = $input->getOption('server');
        }

        // Retrieving info if overlapped host addresses should be overwritten or not.
        $l_overwrite_host_addresses = (bool)$input->getOption('overwriteHost');

        if (!is_numeric($l_jdisc_server)) {
            $output->writeln("No jdisc server selected. Using default server for import.");
            $l_res_jdisc_server = $l_jdisc->get_jdisc_servers();
            while ($l_row = $l_res_jdisc_server->get_row()) {
                if ($l_row['isys_jdisc_db__default_server'] > 0) {
                    $l_jdisc_server = $l_row['isys_jdisc_db__id'];
                    break;
                }
            }

            if (!$l_jdisc_server) {
                $output->writeln("No Server found. Please specify a default server or select an active server.");

                return Command::FAILURE;
            }
            $l_jdisc->switch_database($l_jdisc_server);
        } else {
            $l_jdisc_server = (int)$l_jdisc_server;

            if (!$l_jdisc->switch_database($l_jdisc_server)) {
                $output->writeln("Could not connect to the selected JDisc server. Please confirm if the credentials for this server are correct.");

                return Command::FAILURE;
            }
        }

        if (!is_numeric($l_profile)) {
            $output->writeln("Profile ID has to be from type Integer.");

            return Command::FAILURE;
        } else {
            $output->writeln('Checking Profile ID.');
            $l_profile = (int)$l_profile;

            if (!$l_jdisc->check_profile($l_profile)) {
                $output->writeln("Specified profile ID does not exist.", true);

                return Command::FAILURE;
            }

            if (is_numeric($l_jdisc_server)) {
                if (!$l_jdisc->check_profile_in_server($l_profile, $l_jdisc_server)) {
                    $output->writeln("Specified profile ID is not assigned to the selected JDisc server. Please use another profile or assign the profile to the selected JDisc server.");

                    return Command::FAILURE;
                }
            }
        }

        // Retrieving the mode.
        $importMode = (int)($input->getOption('mode') ?: isys_import_handler_cmdb::C__UPDATE);
        // Groups are optional, profiles not.
        $l_group = (is_numeric($input->getOption('group')) ? $input->getOption('group') : null);
        $l_clear_options['clear_identifiers'] = false;
        $l_clear_options['clear_single_identifier'] = false;

        // Retrieving indicator if search index should be regenerated
        $l_regenerate_search_index = (bool)$input->getOption('regenerateSearchIndex');

        switch ($importMode) {
            case isys_module_jdisc::C__IMPORT_MODE__CREATE:
                $mode = isys_import_handler_cmdb::C__APPEND;
                break;
            case isys_module_jdisc::C__IMPORT_MODE__OVERWRITE_NEW_DISCOVERY:
                $l_clear_options['clear_identifiers'] = true;
                // no break
            case isys_module_jdisc::C__IMPORT_MODE__OVERWRITE:
                $l_jdisc->set_clear_mode(isys_import_handler_cmdb::C__OVERWRITE);
                $mode = isys_import_handler_cmdb::C__UPDATE;
                break;
            case isys_module_jdisc::C__IMPORT_MODE__UPDATE_NEW_DISCOVERY:
                $l_clear_options['clear_identifiers'] = true;
                $mode = isys_import_handler_cmdb::C__UPDATE;
                break;
            case isys_module_jdisc::C__IMPORT_MODE__CREATE_ONLY_NEW_DEVICES: // @see ID-7392 option append new only
                $l_clear_options['clear_identifiers'] = true;
                $mode = isys_import_handler_cmdb::C__APPEND;
                break;
            case isys_module_jdisc::C__IMPORT_MODE__UPDATE_EXISTING_ONLY:
                $l_clear_options['clear_identifiers'] = false;
                $mode = isys_import_handler_cmdb::C__UPDATE;
                break;
            case isys_module_jdisc::C__IMPORT_MODE__UPDATE:
            default:
                $mode = isys_import_handler_cmdb::C__UPDATE;
                break;
        }

        // Prepare the import-array.
        $output->writeln('Begin to retrieve data and prepare the environment...');
        $l_jdisc->set_mode($mode)
            ->prepare_environment($l_profile, null, $this->log);

        // Set up IP Filter
        $l_ip_filter = $input->getOption('ipFilter');
        if (!empty($l_ip_filter)) {
            $l_jdisc->prepare_filter(
                'filter_hostaddress',
                $this->getIPFilterData($l_ip_filter)
            );
        }

        $l_is_jedi = $l_jdisc->is_jedi();

        // Getting the PDO.
        $output->writeln('Receiving the PDO instance... ');
        $l_pdo = null;
        try {
            $l_pdo = $l_jdisc->get_connection($l_jdisc_server);
            $output->writeln('Success!', false);
        } catch (Exception $e) {
            $output->writeln('Failure: ' . $e->getMessage());
        }

        // Retrieve the result set for the objects to be imported.
        $output->writeln('Receiving the JDisc data... ');
        $l_obj_res = $l_jdisc->retrieve_object_result($l_group, $l_profile);

        if ($l_obj_res) {
            try {
                $l_total_objects = $l_pdo->num_rows($l_obj_res);
                $l_start_time = microtime(true);

                // Display the number of objects, that have been found.
                $output->writeln('Found ' . $l_total_objects . ' objects! Collecting Data ...', true);

                // Create an instance of the CMDB import
                $l_import = new isys_import_handler_cmdb($this->log, $this->container->get('database'), isys_cmdb_dao_jdisc::instance($this->container->get('database')));
                $l_import->set_empty_fields_mode(isys_import_handler_cmdb::C__KEEP);

                // Decide if overlapping host addresses should be overwritten or not
                $l_import->set_overwrite_ip_conflicts($l_overwrite_host_addresses);
                $l_import->set_general_header('JDisc');
                $l_import->set_logbook_source(defined_or_default('C__LOGBOOK_SOURCE__JDISC'));

                // Matching from JDisc device id to i-doit object id:
                $l_jdisc_to_idoit = [];

                // Cached object identifiers:
                $l_object_ids = [];

                // Cached devices
                $l_arr_device_ids = [];

                $l_raw_assignments = $l_jdisc->m_dao->get_object_type_assignments_by_profile($l_profile);
                $l_device_arr      = $l_jdisc->prepare_devices($l_obj_res, $l_clear_options, $l_raw_assignments);
                $lastScannedDevice = $l_jdisc->getLastScannedItem();

                // see @ID-7392 import only new jdisc objects
                if ($importMode === isys_module_jdisc::C__IMPORT_MODE__CREATE_ONLY_NEW_DEVICES) {
                    $l_device_arr = array_filter($l_device_arr, function ($device) use ($lastScannedDevice) {
                        return $device['discoverytime'] > $lastScannedDevice && !$device['identifierObjID'];
                    });
                }

                // see @ID-7763 update only new jdisc objects
                if ($importMode === isys_module_jdisc::C__IMPORT_MODE__UPDATE_EXISTING_ONLY) {
                    foreach ($l_device_arr as $l_device_key => $l_device) {
                        if (!$l_device['identifierObjID']) {
                            unset($l_device_arr[$l_device_key]);
                        }
                    }
                }

                $l_import_count = count($l_device_arr);

                if ($l_import_count > 0) {
                    /**
                     * Disconnect the onAfterCategoryEntrySave event to not always reindex the object in every category
                     * This is extremely important!
                     *
                     * An Index is done for all objects at the end of the request, if enabled via parameter.
                     */
                    SearchIndexSignals::instance()
                        ->disconnectOnAfterCategoryEntrySave();

                    $l_not_defined_types = [];
                    $l_already_used = new isys_array();
                    $simpleDatabaseModel = isys_jdisc_dao_software::instance($this->container->get('database'))->isSimpleDatabaseModel();

                    // Retrieve devices

                    if ($l_import_count !== $l_total_objects) {
                        $output->writeln("Importing {$l_import_count} newly discovered objects! Processing Data ...", true);
                    } else {
                        $output->writeln("Importing all {$l_total_objects} objects! Processing Data ...", true);
                    }

                    // @see ID-7391 transform imported objects titles
                    $l_device_arr = $l_jdisc->transformJdiscObjectsTitles($l_profile, $l_device_arr);
                    // END @see ID-7391 transform imported objects titles

                    $jdiscDaoImport = (new isys_jdisc_dao_import($this->container->get('database'), $l_jdisc, $l_clear_options))
                        ->setOutput($output);
                    $currentStats = $jdiscDaoImport->getCurrentStats();

                    //while ($l_obj_row = $l_pdo->fetch_row_assoc($l_obj_res))
                    foreach ($l_device_arr as $l_obj_row) {
                        unset($l_prepared_data);

                        if (!isset($l_obj_row['idoit_obj_type']) || $l_obj_row['idoit_obj_type'] === null) {
                            if (!isset($l_not_defined_types[$l_obj_row['type_name']])) {
                                $output->writeln('JDisc type "' . $l_obj_row['type_name'] . '" is not properly defined in the profile. Skipping devices with JDisc type "' .
                                    $l_obj_row['type_name'] . '".');
                                $l_not_defined_types[$l_obj_row['type_name']] = true;
                            }
                            continue;
                        }

                        if (in_array($l_obj_row['deviceid'], $l_arr_device_ids)) {
                            continue;
                        }

                        $l_import_counter++;

                        $l_arr_device_ids[] = $l_obj_row['deviceid'];

                        $l_prepared_data = $l_jdisc->prepare_object_data($l_obj_row, $l_jdisc_to_idoit, $l_object_ids);

                        $output->writeln('Importing object "' . $l_obj_row['name'] . '": ' . $l_import_counter . '/' . $l_import_count . '.', true);

                        if (!isset($l_prepared_data['object']) || !is_array($l_prepared_data['object'])) {
                            $l_prepared_data['object'] = [];
                        }
                        if (!isset($l_prepared_data['connections']) || !is_array($l_prepared_data['connections'])) {
                            $l_prepared_data['connections'] = [];
                        }
                        if (!isset($l_object_ids) || !is_array($l_object_ids)) {
                            $l_object_ids = [];
                        }

                        Context::instance()
                            ->setContextTechnical(Context::CONTEXT_IMPORT_XML)
                            ->setGroup(Context::CONTEXT_GROUP_IMPORT)
                            ->setContextCustomer(Context::CONTEXT_IMPORT_JDISC)
                            ->setImmutable(true);

                        // Prepare and import the data.
                        $l_import->reset()
                            ->set_scantime()
                            ->set_prepared_data($l_prepared_data['object'])
                            ->set_connection_info($l_prepared_data['connections'])
                            ->set_mode($mode)
                            ->set_object_created_by_others(true)
                            ->set_object_ids($l_object_ids)
                            ->set_logbook_entries(isys_jdisc_dao_devices::instance($this->container->get('database'))
                                ->get_logbook_entries())
                            ->import();
                        $output->writeln('Done!', false);

                        // The last id is the prepared object:
                        $l_last_object_id = $l_import::get_stored_objectID();
                        $l_already_used[$l_last_object_id] = $l_obj_row['name'];

                        $l_jdisc_to_idoit[$l_obj_row['id']] = $l_last_object_id;
                        isys_jdisc_dao_devices::instance($this->container->get('database'))
                            ->set_jdisc_to_idoit_objects($l_obj_row['id'], $l_last_object_id);
                        $l_object_ids[$l_last_object_id] = $l_last_object_id;

                        /* Update CMDB Status */
                        if (isset($l_prepared_data['object'][$l_last_object_id]['cmdb_status']) && $l_prepared_data['object'][$l_last_object_id]['cmdb_status'] > 0) {
                            isys_cmdb_dao::instance($this->container->get('database'))
                                ->set_object_cmdb_status($l_last_object_id, $l_prepared_data['object'][$l_last_object_id]['cmdb_status']);
                        }
                        $lastScannedDevice = $l_obj_row['discoverytime'] > $lastScannedDevice
                            ? $l_obj_row['discoverytime'] : $lastScannedDevice;
                    }
                    $l_jdisc->updateLastScannedDeviceTime($lastScannedDevice);

                    $l_import->set_overwrite_ip_conflicts(false);
                    unset($l_import);

                    Context::instance()
                        ->setImmutable(false)
                        ->setContextTechnical(Context::CONTEXT_DAO_UPDATE)
                        ->setGroup(Context::CONTEXT_GROUP_IMPORT)
                        ->setContextCustomer(Context::CONTEXT_IMPORT_JDISC)
                        ->setImmutable(true);

                    $jdiscDaoImport->executeFinalImportStep($l_jdisc_to_idoit, $l_already_used);

                    $l_affected_categories = $l_jdisc->get_cached_profile()['categoriesConstants'];
                    if (is_array($l_affected_categories) && count($l_affected_categories)) {
                        $startTimeIndexCreation = microtime(true);

                        /* Adding additional categories*/
                        $l_categories = $l_affected_categories;
                        if (defined('C__CATG__NETWORK_LOG_PORT')) {
                            $l_categories[] = 'C__CATG__NETWORK_LOG_PORT';
                        }
                        if (defined('C__CATG__JDISC_CA')) {
                            $l_categories[] = 'C__CATG__JDISC_CA';
                        }
                        if (defined('C__CATS__ACCESS_POINT')) {
                            $l_categories[] = 'C__CATS__ACCESS_POINT';
                        }

                        // Regenerate Search index
                        if ($l_regenerate_search_index) {
                            $output->writeln('Regenerating search index..');
                            SearchIndexSignals::instance()->setOutput($output);
                            SearchIndexSignals::instance()
                                ->onPostImport($l_start_time, $l_categories, filter_defined_constants([
                                    'C__CATS__SERVICE',
                                    'C__CATS__APPLICATION',
                                    'C__CATS__DATABASE_SCHEMA',
                                    'C__CATS__DBMS',
                                    'C__CATS__DATABASE_INSTANCE',
                                    'C__CATS__ACCESS_POINT',
                                    'C__CATS__NET'
                                ]));

                            $output->writeln("Index creation took " . number_format(microtime(true) - $startTimeIndexCreation, 2) . " secs.");
                        }
                    }

                    $output->writeln('Complete process took: ' . isys_glob_seconds_to_human_readable((int)(microtime(true) - $l_start_time)));
                    $output->writeln('Memory peak usage: ' . number_format(memory_get_peak_usage() / 1024 / 1024, 2, '.', '') . ' MB');
                } else {
                    if ($importMode === isys_module_jdisc::C__IMPORT_MODE__CREATE_ONLY_NEW_DEVICES) {
                        $output->writeln('No new scanned devices found, sorry.', true);
                    } else {
                        $output->writeln('No objects found, sorry.', true);
                    }
                }

                $output->writeln('Import finished.', true);
                $output->writeln($l_import_counter . ' objects affected.', true);

                $this->log->info('Import finished.');
            } catch (Exception $e) {
                $l_error_msg = $e->getMessage() . '. File: ' . $e->getFile() . ' Line: ' . $e->getLine();
                $output->writeln('Import failed with message: ');
                $output->writeln($l_error_msg);
                $this->log->error('Import failed with message: ' . $l_error_msg);
                return Command::FAILURE;
            }
        } else {
            $output->writeln('Import failed with message: ');
            $output->writeln('"There are no object types defined in the JDisc profile or are deactivated in the object type configuration."');
            $this->log->error('Import failed with message: "There are no object types defined in the JDisc profile or are deactivated in the object type configuration."');
            return Command::FAILURE;
        }

        try {
            if ($l_jdisc->importCloudSubscriptions) {
                $database = isys_application::instance()->database;
                $l_cloudSubscriptionsDao = isys_cmdb_dao_category_g_cloud_subscriptions::factory($database);
                if ($l_cloudSubscriptionsDao->importSubscriptions($l_jdisc)) {
                    $output->writeln('Cloud Subscriptions import succeeded.', true);
                } else {
                    $output->writeln('Cloud Subscriptions were not imported.', true);
                }
            }
        } catch (Exception $e) {
            $l_error_msg = $e->getMessage() . '. File: ' . $e->getFile() . ' Line: ' . $e->getLine();
            $output->writeln('Cloud Subscriptions Import failed with message: ');
            $output->writeln($l_error_msg);
            $this->log->error('Cloud Subscriptions Import failed with message: ' . $l_error_msg);
            return Command::FAILURE;
        }

        $this->log->info($l_import_counter . ' objects affected.');
        return Command::SUCCESS;
    }

    /**
     * Retrieve Command InputDefinition
     *
     * @return InputDefinition
     */
    public function getCommandDefinition()
    {
        $definition = new InputDefinition();

        $definition->addOption(new InputOption('profile', 'r', InputOption::VALUE_REQUIRED, 'Jdisc Profile ID'));

        $definition->addOption(new InputOption('group', 'g', InputOption::VALUE_OPTIONAL, 'Group ID'));

        $definition->addOption(new InputOption(
            'mode',
            'x',
            InputOption::VALUE_REQUIRED,
            <<<CMD_DEFINITION
            Possible modes are:
            1: Append - The import will only create new objects.
            2: Update - The import will try to update already existing objects.
            3: Overwrite - The import behaves like the update mode but clears all list categories of the existing object.
            4: Update (newly discovered) - The import clears all existing identification keys before the Update mode is triggered.
            5: Overwrite (newly discovered) - The import clears all existing identification keys before the Overwrite mode is triggered.
            6: Only create newly scanned devices - The import creates only newly scanned jdisc devices, existing ones are skipped.
            7: Update (Existing) - Only existing objects will be updated. No new objects are created.
            CMD_DEFINITION
        ));

        $definition->addOption(new InputOption('server', 's', InputOption::VALUE_REQUIRED, 'Jdisc Server ID'));

        $definition->addOption(new InputOption('overwriteHost', 'o', InputOption::VALUE_NONE, 'Indicator for overwriting overlapped host addresses'));

        $definition->addOption(
            new InputOption(
                'detailedLogging',
                'l',
                InputOption::VALUE_OPTIONAL,
                "Possible log-levels are:\n" . "1: low log level only notices and warnings are being logged\n" .
                "2: additionally to the low log level errors are being logged\n" .
                "3: additionally to the normal log level debug messages are being logged. (Memory intensive)\n"
            )
        );

        $definition->addOption(new InputOption('regenerateSearchIndex', 'b', InputOption::VALUE_NONE, 'Regenerate search index after successful import'));

        $definition->addOption(new InputOption('listProfiles', null, InputOption::VALUE_NONE, 'List all available profiles'));

        $definition->addOption(new InputOption(
            'ipFilter',
            null,
            InputOption::VALUE_OPTIONAL,
            'IP filter, use a comma-separated string or newline-separated file with addresses.'
        ));

        return $definition;
    }

    /**
     * Get description for command
     *
     * @return string
     */
    public function getCommandDescription()
    {
        return 'Imports data from a JDisc server (SQL server access is defined in the GUI)';
    }

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

    /**
     * Returns an array of command usages
     *
     * @return string[]
     */
    public function getCommandUsages()
    {
        return [];
    }

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

    private function listProfiles($jDiscProfiles, OutputInterface $output)
    {
        $output->writeln('<info>JDisc Profiles:</info>');
        foreach ($jDiscProfiles as $profile) {
            $output->writeln('  ' . $profile['id'] . '. ' . $profile['title']);
        }
    }

    /**
     * @param string $ipFilter
     *
     * @return string comma separated IP list
     */
    private function getIPFilterData(string $ipFilter): string
    {
        if ($ipList = file($ipFilter)) {
            return implode(
                ',',
                array_map(
                    function (string $value): string {
                        return trim($value);
                    },
                    $ipList
                )
            );
        }
        return $ipFilter;

    }
}
