<?php

/**
 * This program 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; under version 2
 * of the License (non-upgradable).
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2014-2018 (original work) Open Assessment Technologies SA;
 *
 *
 */

use oat\generis\persistence\PersistenceManager;
use oat\oatbox\action\Action;
use oat\oatbox\log\logger\TaoLog;
use oat\oatbox\log\LoggerService;
use oat\oatbox\service\ConfigurableService;
use oat\oatbox\service\ServiceManager;
use Zend\ServiceManager\ServiceLocatorAwareInterface;

class tao_install_Setup implements Action
{
    // Adding container and logger.
    use \oat\oatbox\log\ContainerLoggerTrait;

    /**
     * Setup related dependencies will be reached under this offset.
     */
    const CONTAINER_INDEX = 'taoInstallSetup';

    /**
     * The setup json content offset in the container.
     */
    const SETUP_JSON_CONTENT_OFFSET = 'setupJsonContentOffset';

    /**
     * @param mixed $params The setup params.
     *
     * @throws ErrorException When a module is missing or other kind of general error.
     * @throws common_Exception When the presented config file does not exist
     * @throws common_exception_Error
     * @throws common_ext_ExtensionException When a presented parameter is invalid or malformed.
     * @throws InvalidArgumentException
     * @throws tao_install_utils_Exception
     */
    public function __invoke($params)
    {
        // Using the container if it's necessary with automatic dependency returning.
        $params = $this->initContainer($params, static::CONTAINER_INDEX);

        $this->logNotice('Installing TAO...');

        if ($this->getContainer() !== null && $this->getContainer()->offsetExists(static::SETUP_JSON_CONTENT_OFFSET)) {
            $parameters = json_decode($this->getContainer()->offsetGet(static::SETUP_JSON_CONTENT_OFFSET), true);
            if (is_null($parameters)) {
                throw new InvalidArgumentException('Your Setup JSON seed is malformed');
            }
        } else {
            if (!isset($params[0])) {
                throw new InvalidArgumentException('You should provide a file path');
            }

            $filePath = $params[0];

            if (!file_exists($filePath)) {
                throw new \ErrorException('Unable to find ' . $filePath);
            }

            $info = pathinfo($filePath);

            switch ($info['extension']) {
                case 'json':
                    $parameters = json_decode(file_get_contents($filePath), true);
                    if (is_null($parameters)) {
                        throw new InvalidArgumentException('Your JSON file is malformed');
                    }
                    break;
                case 'yml':
                    if (extension_loaded('yaml')) {
                        $parameters = \yaml_parse_file($filePath);
                        if ($parameters === false) {
                            throw new InvalidArgumentException('Your YAML file is malformed');
                        }
                    } else {
                        throw new ErrorException('Extension yaml should be installed');
                    }
                    break;
                default:
                    throw new InvalidArgumentException('Please provide a JSON or YAML file');
            }
        }

        /** @var LoggerService $loggerService */
        $loggerService = $this->getContainer()->offsetGet(LoggerService::SERVICE_ID);
        $loggerService->addLogger(
            new TaoLog([
                'appenders' => [
                    [
                        'class' => 'SingleFileAppender',
                        'threshold' => common_Logger::TRACE_LEVEL,
                        'file' => TAO_INSTALL_PATH . 'tao/install/log/install.log'
                    ]
                ]
            ])
        );

        $options =  [
             "install_sent"    =>  "1"
            , "module_host" =>      "tao.local"
            , "module_lang" =>      "en-US"
            , "module_mode" =>      "debug"
            , "module_name" =>      "mytao"
            , "module_namespace" => ""
            , "module_url"  =>      ""
            , "submit"  =>          "Install"
            , "user_email"  =>      ""
            , "user_firstname"  =>  ""
            , "user_lastname"   =>  ""
            , "user_login"  =>      ""
            , "user_pass"   =>      ""
            , "instance_name" =>    null
            , "extensions" =>       null
            , 'timezone'   =>      date_default_timezone_get()
            , 'extra_persistences' => []
        ];

        if (!isset($parameters['configuration'])) {
            throw new InvalidArgumentException('Your config should have a \'configuration\' key');
        }

        if (!isset($parameters['configuration']['generis'])) {
            throw new InvalidArgumentException('Your config should have a \'generis\' key under \'configuration\'');
        }

        if (!isset($parameters['configuration']['global'])) {
            throw new InvalidArgumentException('Your config should have a \'global\' key under \'configuration\'');
        }

        $global = $parameters['configuration']['global'];
        $options['module_namespace'] = $global['namespace'];
        $options['instance_name'] = $global['instance_name'];
        $options['module_url'] = $global['url'];
        $options['module_lang'] = $global['lang'];
        $options['module_mode'] = $global['mode'];
        $options['timezone'] = $global['timezone'];
        $options['import_local'] = (isset($global['import_data']) && $global['import_data'] === true);

        $rootDir = dir(dirname(__FILE__) . '/../../');
        $options['root_path'] = isset($global['root_path'])
            ? $global['root_path']
            : realpath($rootDir->path) . DIRECTORY_SEPARATOR;

        $options['file_path'] = isset($global['file_path'])
            ? $global['file_path']
            : $options['root_path'] . 'data' . DIRECTORY_SEPARATOR;

        if (isset($global['session_name'])) {
            $options['session_name'] = $global['session_name'];
        }

        if (isset($global['anonymous_lang'])) {
            $options['anonymous_lang'] = $global['anonymous_lang'];
        }

        //get extensions to install
        if (isset($parameters['extensions'])) {
            $options['extensions'] = $parameters['extensions'];
        }

        if (!isset($parameters['super-user'])) {
            throw new InvalidArgumentException('Your config should have a \'global\' key under \'generis\'');
        }

        $superUser = $parameters['super-user'];
        $options['user_login'] = $superUser['login'];
        $options['user_pass1'] = $superUser['password'];
        if (isset($parameters['lastname'])) {
            $options['user_lastname'] = $parameters['lastname'];
        }
        if (isset($parameters['firstname'])) {
            $options['user_firstname'] = $parameters['firstname'];
        }
        if (isset($parameters['email'])) {
            $options['user_email'] = $parameters['email'];
        }


        $installOptions = [
            'root_path'     => $options['root_path'],
            'install_path'  => $options['root_path'] . 'tao/install/',
        ];

        if (isset($global['installation_config_path'])) {
            $installOptions['installation_config_path'] = $global['installation_config_path'];
        }

        // run the actual install
        if ($this->getContainer() instanceof \Pimple\Container) {
            $this->getContainer()->offsetSet(\tao_install_Installator::CONTAINER_INDEX, $installOptions);
            $installator = new \tao_install_Installator($this->getContainer());
        } else {
            $installator = new \tao_install_Installator($installOptions);
        }

        $serviceManager = $installator->getServiceManager();

        if (!isset($parameters['configuration']['generis']['persistences'])) {
            throw new InvalidArgumentException('Your config should have a \'persistence\' key under \'generis\'');
        }
        $persistences = $parameters['configuration']['generis']['persistences'];
        if (isset($persistences['default'])) {
            $parameters['configuration']['generis']['persistences'] = $this->wrapPersistenceConfig($persistences);
        } elseif (!isset($persistences['type'])) {
            throw new InvalidArgumentException('Your config should have a \'default\' key under \'persistences\'');
        }

        foreach ($parameters['configuration'] as $extension => $configs) {
            foreach ($configs as $key => $config) {
                if (isset($config['type']) && $config['type'] === 'configurableService') {
                    $className = $config['class'];
                    $params = $config['options'];
                    if (is_a($className, \oat\oatbox\service\ConfigurableService::class, true)) {
                        if (is_a($className, \oat\tao\model\service\InjectionAwareService::class, true)) {
                            $service = new $className(...$this->prepareParameters($className, $params, $serviceManager));
                        } else {
                            $service = new $className($params);
                        }
                        $serviceManager->register($extension . '/' . $key, $service);
                    } else {
                        $this->logWarning('The class : ' . $className . ' can not be set as a Configurable Service');
                        $this->logWarning('Make sure your configuration is correct and all required libraries are installed');
                    }
                }
            }
        }

        // mod rewrite cannot be detected in CLI Mode.
        $installator->escapeCheck('custom_tao_ModRewrite');
        $logger = $this->getLogger();

        $installator->install($options, function () use ($serviceManager, $parameters, $logger) {
            /** @var common_ext_ExtensionsManager $extensionManager */
            $extensionManager = $serviceManager->get(common_ext_ExtensionsManager::SERVICE_ID);
            foreach ($parameters['configuration'] as $ext => $configs) {
                foreach ($configs as $key => $config) {
                    if (! (isset($config['type']) && $config['type'] === 'configurableService')) {
                        if (! is_null($extensionManager->getInstalledVersion($ext))) {
                            $extension = $extensionManager->getExtensionById($ext);
                            if (! $extension->hasConfig($key) || ! $extension->getConfig($key) instanceof ConfigurableService) {
                                if (! $extension->setConfig($key, $config)) {
                                    throw new ErrorException('Your config ' . $ext . '/' . $key . ' cannot be set');
                                }
                            }
                        }
                    }
                }
            }

            // execute post install scripts
            if (isset($parameters['postInstall'])) {
                foreach ($parameters['postInstall'] as $script) {
                    if (isset($script['class']) && is_a($script['class'], Action::class, true)) {
                        $object = new $script['class']();
                        if (is_a($object, ServiceLocatorAwareInterface::class)) {
                            $object->setServiceLocator($serviceManager);
                        }
                        $params = (isset($script['params']) && is_array($script['params'])) ? $script['params'] : [];
                        $report = call_user_func($object, $params);

                        if ($report instanceof common_report_Report) {
                            $logger->info(helpers_Report::renderToCommandline($report));
                        }
                    }
                }
            }
            $logger->notice('Installation completed!');
        });

    }

    /**
     * @param string         $class
     * @param array          $parametersToSort
     * @param ServiceManager $serviceManager
     *
     * @return array
     * @throws ReflectionException
     */
    private function prepareParameters(string $class, array $parametersToSort, ServiceManager $serviceManager): array
    {
        $reflectionClass = new ReflectionClass($class);

        $constructParameters = $reflectionClass->getMethod('__construct')->getParameters();

        $sortedParameters = [];

        while($constructParameters && $parametersToSort) {
            $parameter     = array_shift($constructParameters);
            $parameterName = $parameter->getName();

            try {
                $paramValue = $parametersToSort[$parameterName] ?? $parameter->getDefaultValue();

                $sortedParameters[] = $this->resolveParameter($parameter, $paramValue, $serviceManager);

                unset($parametersToSort[$parameterName]);
            } catch (ReflectionException $exception) {
                throw new RuntimeException(
                    sprintf('No default value for `$%s` argument in %s::__construct', $parameterName, $class)
                );
            }
        }

        if ($parametersToSort) {
            throw new InvalidArgumentException(
                sprintf('Invalid arguments `%s` specified for %s', implode(', ', array_keys($parametersToSort)), $class)
            );
        }

        return $sortedParameters;
    }

    private function resolveParameter(ReflectionParameter $parameter, $paramValue, ServiceManager $serviceManager)
    {
        if (
            is_string($paramValue)
            && $parameter->getClass() !== null
            && $serviceManager->has($paramValue)
        ) {
            $paramValue = $serviceManager->get($paramValue);
        }

        return $paramValue;
    }

    /**
     * Transforms the seed persistence configuration into command line parameters
     * and then back into a persistence configuration to ensure backwards compatibility
     * with the previous process
     * @param array $persistences
     * @return array
     */
    private function wrapPersistenceConfig($persistences)
    {
        $installParams = $this->getCommandLineParameters($persistences['default']);

        $dbalConfigCreator = new tao_install_utils_DbalConfigCreator();
        $persistences['default'] = $dbalConfigCreator->createDbalConfig($installParams);

        return [
            'type' => 'configurableService',
            'class' => PersistenceManager::class,
            'options' => [
                'persistences' => $persistences,
            ],
        ];
    }

    private function getCommandLineParameters(array $defaultPersistenceConfig): array
    {
        if (isset($defaultPersistenceConfig['connection'])) {
            if ($this->isMasterSlaveConnection($defaultPersistenceConfig)) {
                $options['db_driver'] = $defaultPersistenceConfig['connection']['driver'];
                $options['db_host'] = $defaultPersistenceConfig['connection']['master']['host'];
                $options['db_name'] = $defaultPersistenceConfig['connection']['master']['dbname'];

                if (isset($defaultPersistenceConfig['connection']['master']['user'])) {
                    $options['db_user'] = $defaultPersistenceConfig['connection']['master']['user'];
                }

                if (isset($defaultPersistenceConfig['connection']['master']['password'])) {
                    $options['db_pass'] = $defaultPersistenceConfig['connection']['master']['password'];
                }
            } else {
                $options['db_driver'] = $defaultPersistenceConfig['connection']['driver'];

                if (isset($defaultPersistenceConfig['connection']['driverClass'])) {
                    $options['db_driverClass'] = $defaultPersistenceConfig['connection']['driverClass'];
                }

                if (isset($defaultPersistenceConfig['connection']['driverOptions'])) {
                    $options['db_driverOptions'] = $defaultPersistenceConfig['connection']['driverOptions'];
                }

                if (isset($defaultPersistenceConfig['connection']['instance'])) {
                    $options['db_instance'] = $defaultPersistenceConfig['connection']['instance'];
                }

                $options['db_host'] = $defaultPersistenceConfig['connection']['host'];
                $options['db_name'] = $defaultPersistenceConfig['connection']['dbname'];

                if (isset($defaultPersistenceConfig['connection']['user'])) {
                    $options['db_user'] = $defaultPersistenceConfig['connection']['user'];
                }

                if (isset($defaultPersistenceConfig['connection']['password'])) {
                    $options['db_pass'] = $defaultPersistenceConfig['connection']['password'];
                }
            }
        } else {
            $options['db_driver'] = $defaultPersistenceConfig['driver'];
            $options['db_host'] = $defaultPersistenceConfig['host'];
            $options['db_name'] = $defaultPersistenceConfig['dbname'];

            if (isset($defaultPersistenceConfig['user'])) {
                $options['db_user'] = $defaultPersistenceConfig['user'];
            }

            if (isset($defaultPersistenceConfig['password'])) {
                $options['db_pass'] = $defaultPersistenceConfig['password'];
            }
        }

        return $options;
    }

    private function isMasterSlaveConnection(array $defaultPersistenceConfig): bool
    {
        return isset($defaultPersistenceConfig['connection']['wrapperClass'])
            && $defaultPersistenceConfig['connection']['wrapperClass'] === '\\Doctrine\\DBAL\\Connections\\MasterSlaveConnection';
    }
}