<?php

namespace idoit\Console\Command\Import\Csv;

use idoit\Component\Logger;
use idoit\Console\Command\AbstractCommand;
use idoit\Console\Exception\MissingFileException;
use isys_cache;
use isys_format_json;
use isys_module_import_csv;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
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 ImportCsvCommand extends AbstractCommand
{
    const NAME = 'import-csv';

    private const MULTI_VALUE_MODES = [
        'row',
        'column',
        'comma'
    ];

    private const MULTI_VALUE_UPDATE_MODES = [
        'create-if-empty' => isys_module_import_csv::CL__MULTIVALUE_MODE__UNTOUCHED,
        'add' => isys_module_import_csv::CL__MULTIVALUE_MODE__ADD,
        'replace' => isys_module_import_csv::CL__MULTIVALUE_MODE__OVERWRITE
    ];

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

    /**
     * Get description for command
     *
     * @return string
     */
    public function getCommandDescription()
    {
        return 'Imports CSV formatted files (Using a predefined CSV Import filter, defined in the GUI)';
    }

    /**
     * Retrieve Command InputDefinition
     *
     * @return InputDefinition
     */
    public function getCommandDefinition()
    {
        $multiValueModes = '"<comment>' . implode('</comment>", "<comment>', self::MULTI_VALUE_MODES) . '</comment>"';
        $multiValueUpdateModes = '"<comment>' . implode('</comment>", "<comment>', array_keys(self::MULTI_VALUE_UPDATE_MODES)) . '</comment>"';

        $definition = new InputDefinition();
        $definition->addOption(new InputOption('importFile', null, InputOption::VALUE_REQUIRED, 'CSV file for import'));
        $definition->addOption(new InputOption('importProfileId', null, InputOption::VALUE_REQUIRED, 'Profile which should be used to map import file'));
        $definition->addOption(new InputOption('csvSeparator', null, InputOption::VALUE_REQUIRED, 'Separator of import file'));
        $definition->addOption(new InputOption('multiValueMode', null, InputOption::VALUE_REQUIRED, 'Multivalue mode. Possible modes are ' . $multiValueModes));
        $definition->addOption(new InputOption('multi-value-update-mode', null, InputOption::VALUE_REQUIRED, 'Multivalue update mode. Possible modes are ' . $multiValueUpdateModes));
        $definition->addOption(new InputOption('default-template', null, InputOption::VALUE_NONE + InputOption::VALUE_NEGATABLE, 'Define if the default template should be applied'));
        $definition->addOption(new InputOption('headers', null, InputOption::VALUE_NONE + InputOption::VALUE_NEGATABLE, 'Define if headers are present'));
        $definition->addOption(new InputOption('empty-values', null, InputOption::VALUE_NONE + InputOption::VALUE_NEGATABLE, 'Define if empty values should be applied'));

        return $definition;
    }

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

    /**
     * Returns an array of command usages
     *
     * @return string[]
     */
    public function getCommandUsages()
    {
        return [
            '-u admin -p admin -i 1 --importFile /var/www/imports/idoit-demo-csv-import.csv --importProfileId 1',
            '-u admin -p admin -i 1 --importFile /var/www/imports/idoit-demo-csv-import.csv --importProfileId 2 --csvSeparator ";" --multiValueMode column',
            '-u admin -p admin -i 1 --importFile /var/www/imports/idoit-demo-csv-import.csv --importProfileId 3 --multi-value-update-mode replace'
        ];
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $output->writeln('<info>csv-Import handler initialized at </info>' . date('Y-m-d H:i:s'));

        try {
            $this->process($input, $output);
            return Command::SUCCESS;
        } catch (\Exception $e) {
            $output->writeln('<error>' . $e->getMessage() . '</error>');
            return Command::FAILURE;
        }
    }

    /**
     * @param InputInterface  $input
     * @param OutputInterface $output
     *
     * @throws MissingFileException
     * @throws \idoit\Exception\JsonException
     * @throws \isys_exception_database
     */
    private function process(InputInterface $input, OutputInterface $output)
    {
        // Initialize:
        $l_file = $input->getOption('importFile');
        $l_profile_id = $input->getOption('importProfileId');
        $delimiterOption = $input->getOption('csvSeparator');
        $multivalueModeOption = $input->getOption('multiValueMode');
        $multivalueUpdateModeOption = $input->getOption('multi-value-update-mode');
        $useDefaultTemplateOption = $input->getOption('default-template');
        $headersOption = $input->getOption('headers');
        $adoptEmptyValuesOption = $input->getOption('empty-values');
        $l_object_type_assignment = [];
        $l_identificationCounter = 1;

        // First check, if the given file exists.
        if (!file_exists($l_file)) {
            throw new MissingFileException('File "' . $l_file . '" does not exist.');
        }

        // Then check the given profile, other options will be 'preselected' by this but can be overwritten via Command option.
        if ($l_profile_id === null || !is_numeric($l_profile_id)) {
            throw new InvalidArgumentException('Please provide a valid profile ID, run the "import-csvprofiles" command to get a list');
        }

        // Only allow single character separators.
        if ($delimiterOption !== null && mb_strlen($delimiterOption) !== 1) {
            throw new InvalidArgumentException("CSV delimiter must be a single character ('{$delimiterOption}' given)");
        }

        // Load Profile.
        $l_profiles = isys_module_import_csv::get_profiles($l_profile_id);
        $output->writeln('Retrieve profile with id #' . $l_profile_id . '...');

        if (!is_array($l_profiles) || count($l_profiles) === 0) {
            throw new RuntimeException('Unable to load profile with ID #' . $l_profile_id);
        }

        // Get first profile.
        $l_profile = $l_profiles[0];

        // Decode data attribute into array.
        $l_profile['data'] = isys_format_json::decode($l_profile['data']);

        // Check for filled profile
        if (!is_array($l_profile['data'])) {
            throw new RuntimeException('Profile does not have any data.');
        }

        // Some transformation work
        $l_key_data = [];
        $l_transformed_assignment = [];

        $multivalueMode = $l_profile['data']['multivalue'];
        $multivalueUpdateMode = $l_profile['data']['multivalueUpdateMode'];
        $separator = $l_profile['data']['separator'];
        $useDefaultTemplate = $l_profile['data']['defaultTemplate'];
        $headers = $l_profile['data']['headlines'];
        $adoptEmptyValues = $l_profile['data']['singlevalueOverwriteEmptyValues'];

        $output->writeln('Profile <info>' . $l_profile['title'] . '</info> succesfully loaded.');
        $output->writeln(['', '<comment>Show used options</comment>'], OutputInterface::VERBOSITY_VERBOSE);

        $fromProfile = '<comment>from profile</comment>';
        $fromCommandOption = '<comment>from command option</comment>';

        // Check the given multi-value mode.
        if ($multivalueModeOption === null) {
            $output->writeln("  Loaded multi-value mode <info>{$multivalueMode}</info> ({$fromProfile})", OutputInterface::VERBOSITY_VERBOSE);
        } else {
            if (!in_array($multivalueModeOption, self::MULTI_VALUE_MODES)) {
                throw new InvalidArgumentException(sprintf('Given mode "%s" is not allowed, choose one of these: "%s"', $multivalueModeOption, implode('", "', self::MULTI_VALUE_MODES)));
            }

            $output->writeln("  <comment>Overwrite</comment> multi-value mode <info>{$multivalueMode}</info> with <info>{$multivalueModeOption}</info> ({$fromCommandOption})", OutputInterface::VERBOSITY_VERBOSE);
            $multivalueMode = $multivalueModeOption;
        }

        // Check the given multi-value update mode.
        if ($multivalueUpdateModeOption === null) {
            $updateMode = array_search($multivalueUpdateMode, self::MULTI_VALUE_UPDATE_MODES);
            $output->writeln("  Loaded multi-value update mode <info>{$updateMode}</info> ({$fromProfile})", OutputInterface::VERBOSITY_VERBOSE);
        } else {
            if (!isset(self::MULTI_VALUE_UPDATE_MODES[$multivalueUpdateModeOption])) {
                throw new InvalidArgumentException(sprintf('Given mode "%s" is not allowed, choose one of these: "%s"', $multivalueUpdateModeOption, implode('", "', array_keys(self::MULTI_VALUE_UPDATE_MODES))));
            }

            $updateMode = array_search($multivalueUpdateMode, self::MULTI_VALUE_UPDATE_MODES);
            $output->writeln("  <comment>Overwrite</comment> multi-value update mode <info>{$updateMode}</info> with <info>{$multivalueUpdateModeOption}</info> ({$fromCommandOption})", OutputInterface::VERBOSITY_VERBOSE);
            $multivalueUpdateMode = self::MULTI_VALUE_UPDATE_MODES[$multivalueUpdateModeOption];
        }

        // Check the given delimiter.
        if ($delimiterOption === null) {
            $output->writeln("  Loaded separator <info>{$separator}</info> ({$fromProfile})", OutputInterface::VERBOSITY_VERBOSE);
        } else {
            $output->writeln("  <comment>Overwrite</comment> separator <info>{$separator}</info> with <info>{$delimiterOption}</info> ({$fromCommandOption})", OutputInterface::VERBOSITY_VERBOSE);
            $separator = $delimiterOption;
        }

        // Check the given 'use default-template' option.
        if ($useDefaultTemplateOption === null) {
            $output->writeln("  <info>" . ($useDefaultTemplate ? 'Do' : 'Do not') . "</info> consider default template ({$fromProfile})", OutputInterface::VERBOSITY_VERBOSE);
        } else {
            $output->writeln("  <comment>Overwrite</comment> <info>" . ($useDefaultTemplate ? 'do' : 'do not') . "</info> with <info>" . ($useDefaultTemplateOption ? 'do' : 'do not') . "</info> consider default template ({$fromCommandOption})", OutputInterface::VERBOSITY_VERBOSE);
            $useDefaultTemplate = $useDefaultTemplateOption;
        }

        // Check the given 'use default-template' option.
        if ($headersOption === null) {
            $output->writeln("  <info>" . ($headers ? 'Has' : 'Has no') . "</info> headers ({$fromProfile})", OutputInterface::VERBOSITY_VERBOSE);
        } else {
            $output->writeln("  <comment>Overwrite</comment> <info>" . ($headers ? 'has' : 'has no') . "</info> with <info>" . ($headersOption ? 'has' : 'has no') . "</info> headers ({$fromCommandOption})", OutputInterface::VERBOSITY_VERBOSE);
            $headers = $headersOption;
        }

        // Check the given 'use default-template' option.
        if ($adoptEmptyValuesOption === null) {
            $output->writeln("  <info>" . ($adoptEmptyValues ? 'Do' : 'Do not') . "</info> adopt empty values ({$fromProfile})", OutputInterface::VERBOSITY_VERBOSE);
        } else {
            $output->writeln("  <comment>Overwrite</comment> <info>" . ($adoptEmptyValues ? 'do' : 'do not') . "</info> with <info>" . ($adoptEmptyValuesOption ? 'do' : 'do not') . "</info> adopt empty values ({$fromCommandOption})", OutputInterface::VERBOSITY_VERBOSE);
            $adoptEmptyValues = $adoptEmptyValuesOption;
        }

        $output->writeln('', OutputInterface::VERBOSITY_VERBOSE);

        foreach ($l_profile['data']['assignments'] as $l_index => $l_data) {
            if (empty($l_data['category'])) {
                continue;
            }

            // Empty property means we have object_title, category.
            if (!defined($l_data['category']) && empty($l_data['property'])) {
                $l_key_data[$l_data['category']] = $l_index;
            } else {
                if (strpos(isys_module_import_csv::CL__MULTIVALUE_TYPE__COLUMN, $multivalueMode) === false) {
                    $l_transformed_assignment[$l_data['category']][$l_data['property']] = $l_index;
                } else {
                    $l_transformed_assignment[$l_index] = $l_data;
                }

                if (isset($l_data['object_type']) && isset($l_data['create_object'])) {
                    $l_object_type_assignment[$l_index] = [
                        'object-type'   => $l_data['object_type'],
                        'create-object' => (int)$l_data['create_object']
                    ];
                }
            }
        }

        // Collect necessary information for the import process
        $output->writeln('Initializing csv-import...');

        if (defined($l_profile['data']['globalObjectType'])) {
            $l_profile['data']['globalObjectType'] = constant($l_profile['data']['globalObjectType']);
        }

        switch ($output->getVerbosity()) {
            default:
            case $output::VERBOSITY_NORMAL:
                $importVerbosity = Logger::WARNING;
                break;

            case $output::VERBOSITY_VERBOSE:
                $importVerbosity = Logger::NOTICE;
                break;

            case $output::VERBOSITY_VERY_VERBOSE:
                $importVerbosity = Logger::INFO;
                break;

            case $output::VERBOSITY_DEBUG:
                $importVerbosity = Logger::DEBUG;
                break;
        }

        // @see  ID-7076  Fix the additional property search definition.
        $additionalPropertySearch = [];

        if (\is_array($l_profile['data']['assignments']) && \is_array($l_profile['data']['additionalPropertySearch'])) {
            foreach ($l_profile['data']['additionalPropertySearch'] as $index => $definition) {
                if (!isset($l_profile['data']['assignments'][$index]['category'])) {
                    continue;
                }

                $key = $l_profile['data']['assignments'][$index]['category'] . '_' . $index;

                $additionalPropertySearch[$key] = isys_format_json::encode($definition);
            }
        }

        // Initialize csv module
        $l_module_csv = new isys_module_import_csv(
            $l_file,
            $separator,
            $multivalueMode,
            $l_key_data['object_title'],
            $l_profile['data']['globalObjectType'],
            $l_key_data['object_type_dynamic'],
            $l_key_data['object_purpose'],
            $l_key_data['object_category'],
            $l_key_data['object_sysid'],
            $l_key_data['object_cmdbstatus'],
            $l_key_data['object_description'],
            (bool)$headers,
            (bool)$useDefaultTemplate,
            $additionalPropertySearch,
            $multivalueUpdateMode,
            $importVerbosity,
            (bool)$adoptEmptyValues,
        );

        if (is_array($l_profile['data']['identificationKeys']) && count($l_profile['data']['identificationKeys']) > 0) {
            $l_csv_idents = [];
            $l_identifiers = [];

            // See ID-4162
            if (isset($l_profile['data']['identificationCounter']) && $l_profile['data']['identificationCounter'] > 1) {
                $l_identificationCounter = (int)$l_profile['data']['identificationCounter'];
            }

            foreach ($l_profile['data']['identificationKeys'] as $l_data) {
                $l_csv_idents[] = $l_data['csvIdent'];
                $l_identifiers[] = $l_data['localIdent'];
            }

            $l_module_csv->set_matching_csv_identifiers($l_csv_idents);
            $l_module_csv->set_matching_identifiers($l_identifiers);
            // @see  ID-6908  Set the "min match limit" in order to fix the bug.
            $l_module_csv->set_matching_min_match_limit($l_identificationCounter);

            $identificationKeyCount = count($l_profile['data']['identificationKeys']);

            $output->writeln(['', "Found <info>{$identificationKeyCount}</info> identification fields of which at least <info>{$l_identificationCounter}</info> need to match!"]);
        }

        $l_module_csv->initialize($l_transformed_assignment, $l_object_type_assignment);

        $cacheNamespace = "csv-import-info-" . date('Y-m-d');
        // @see ID-9230 Invalidate cache before import. And @see ID-9559 invalidate cache before cache object is set
        $l_module_csv->invalidateCache($cacheNamespace);
        $l_module_csv->setCacheObject(isys_cache::keyvalue()->ns($cacheNamespace));

        // Trigger import.
        $l_module_csv->import();

        // Output log
        if (file_exists($l_module_csv->get_log_path()) && is_readable($l_module_csv->get_log_path())) {
            $output->writeln(file_get_contents($l_module_csv->get_log_path()));
        }

        $output->writeln("Successfully imported data.");
    }
}
