<?php

namespace idoit\Module\Console\Steps\Addon;

use Composer\Semver\Semver;
use Exception;
use idoit\AddOn\AddonVerify;
use idoit\Module\Console\Steps\CollectionStep;
use idoit\Module\Console\Steps\Message\ErrorLevel;
use idoit\Module\Console\Steps\Message\Messages;
use idoit\Module\Console\Steps\Message\StepMessage;
use idoit\Module\Console\Steps\Sql\ExecuteQueryStep;
use idoit\Module\Console\Steps\Step;
use isys_component_constant_manager;
use isys_component_dao_mandator;
use isys_component_database;
use isys_module_manager;
use isys_update_files;
use Symfony\Component\Filesystem\Filesystem;

class AddonInstall implements Step
{
    private $id;

    /**
     * @var isys_module_manager
     */
    private $moduleManager;

    /**
     * @var isys_component_database
     */
    private $systemDb;

    /**
     * @var isys_component_database
     */
    private $tenantDb;

    /**
     * @var int Id of the tenant
     */
    private $tenantId;

    /**
     * @var array|Step[] Steps to undo
     */
    private $toUndo = [];

    private $installed = false;

    private ?string $tempDirectory;

    /**
     * @param string                  $id
     * @param int                     $tenantId
     * @param isys_component_database $systemDb
     *
     * @throws \isys_exception_database
     */
    public function __construct($id, $tenantId, isys_component_database $systemDb, ?string $tempDirectory = null)
    {
        $this->id = $id;
        $this->tenantId = $tenantId;
        $this->systemDb = $systemDb;
        $this->tempDirectory = $tempDirectory;

        $tenantDao = isys_component_dao_mandator::instance($this->systemDb);
        $tenant = $tenantDao->get_mandator($this->tenantId, 0)
            ->get_row();

        if (!is_array($tenant)) {
            return;
        }

        $this->tenantDb = isys_component_database::get_database(
            'mysql',
            $tenant["isys_mandator__db_host"],
            $tenant["isys_mandator__db_port"],
            $tenant["isys_mandator__db_user"],
            isys_component_dao_mandator::getPassword($tenant),
            $tenant["isys_mandator__db_name"]
        );
        $this->moduleManager = new isys_module_manager($this->tenantDb);

        // Include the tenant specific cache.
        isys_component_constant_manager::instance()->include_dcm($tenant["isys_mandator__dir_cache"]);
    }

    /**
     * Get name of the step
     *
     * @return string
     */
    public function getName()
    {
        return 'Install Add-on ' . $this->id . ' for tenant ' . $this->tenantId;
    }

    /**
     * Process the work
     *
     * @param Messages $messages
     *
     * @return mixed
     */
    public function process(Messages $messages)
    {
        $this->toUndo = [];
        $this->installed = false;

        global $g_product_info;

        $targetPath = BASE_DIR . 'src/classes/modules/' . $this->id;
        $filesystem = new Filesystem();
        $addonChecker = new AddonVerify();

        $packageFile = ($this->tempDirectory ?? $targetPath) . '/package.json';

        if (file_exists($packageFile)) {
            $messages->addMessage(new StepMessage($this, 'Found package file "package.json" for add-on ' . $this->id . '!', ErrorLevel::INFO));
        } else {
            $packageFile = ($this->tempDirectory ?? $targetPath) . "/package-{$this->id}.json";
        }

        if (file_exists($packageFile)) {
            if (strpos($packageFile, 'package-') !== false) {
                $messages->addMessage(new StepMessage($this, 'Found bundle package file "package-' . $this->id . '.json" for add-on ' . $this->id . '!', ErrorLevel::INFO));
            }
        } else {
            $messages->addMessage(new StepMessage($this, 'Could not find package file "package.json" or "package-' . $this->id . '.json" for add-on ' . $this->id . '!', ErrorLevel::ERROR));

            return false;
        }

        $package = json_decode(file_get_contents($packageFile), true);

        try {
            // Check if the add-on is allowed to be installed.
            if (isset($package['requirements']['core']) && !Semver::satisfies($g_product_info['version'], $package['requirements']['core'])) {
                [, $version] = explode(' ', $package['requirements']['core']);

                throw new Exception('i-doit Version requirement for this add-on does not match. Update to version ' . $version . ' and try again.');
            }

            // @see ID-9075 Check if the provided add-on is allowed in this i-doit version.
            if (!$addonChecker->canInstall($package['identifier'], $package['version'])) {
                $addonTitle = $package['title'] ?: ucfirst($package['identifier']);
                $compatibleVersion = $addonChecker->getCompatibleVersion($package['identifier']);
                $givenVersion = $package['version'];

                throw new Exception("{$addonTitle} can not be installed, please try to install at least version {$compatibleVersion} (you provided {$givenVersion})");
            }

            // @see ID-9066 The checks are OK, move the files to the correct directory.
            if ($this->tempDirectory !== null) {
                // Prevent an add-on from being downgraded.
                if (file_exists($targetPath . '/package.json')) {
                    $addonInfo = json_decode(file_get_contents($targetPath . '/package.json'), true);

                    // Check if the installed add-on version is bigger than the provided one
                    if (Semver::satisfies($addonInfo['version'], '> ' . $package['version'])) {
                        $addonTitle = $package['title'] ?: ucfirst($package['identifier']);
                        throw new Exception("You can not install a lower version of the add-on '{$addonTitle}', you need to provide a higher or equal version.");
                    }
                }

                // Move add-on specific files.
                $filesystem->mirror($this->tempDirectory . 'src/classes/modules/' . $this->id . '/', $targetPath);
                // @see ID-10719 Use 'copy' instead 'rename' in case this add-on will be installed for another tenant during the same request.
                $filesystem->copy($packageFile, $targetPath . '/package.json', true);
            }

            // Include module installscript if available.
            if (file_exists($targetPath . '/install/isys_module_' . $this->id . '_install.class.php')) {
                include_once($targetPath . '/install/isys_module_' . $this->id . '_install.class.php');
            }

            // Delete files if necessary.
            if (file_exists($targetPath . '/install/update_files.xml')) {
                (new isys_update_files())->delete($targetPath . '/install');
            }

            $id = $this->moduleManager->installAddOn($package, false);
            $this->installed = true;

            if ($id === false) {
                $messages->addMessage(new StepMessage($this, 'Add-on ' . $this->id . ' is not installed for tenant ' . $this->tenantId, ErrorLevel::ERROR));

                return false;
            }

            $messages->addMessage(new StepMessage($this, 'Add-on ' . $this->id . ' is installed for tenant ' . $this->tenantId, ErrorLevel::INFO));

            $step = new CollectionStep('Update databases', [
                new Updater($targetPath . '/install/update_data.xml', $this->tenantDb),
                new Updater($targetPath . '/install/update_sys.xml', $this->systemDb)
            ]);

            if ($step->process($messages)) {
                $this->toUndo[] = $step;
            } else {
                return false;
            }

            // When a package.json already exists, this is an update.
            if (file_exists($packageFile)) {
                $type = 'update';
            } else {
                $type = 'install';
            }

            $moduleClassName = 'isys_module_' . $this->id;
            $updateSettings = false;

            if (class_exists($moduleClassName) && is_a($moduleClassName, 'idoit\AddOn\InstallableInterface', true)) {
                $moduleClassName::install($this->tenantDb, $this->systemDb, $id, $type, $this->tenantId);
                $updateSettings = true;
            } else {
                // Call module installscript if available.
                $installClass = 'isys_module_' . $this->id . '_install';

                if (class_exists($installClass)) {
                    call_user_func([$installClass, 'init'], $this->tenantDb, $this->systemDb, $id, $type, $this->tenantId);

                    $updateSettings = true;
                }
            }

            if ($updateSettings) {
                $step = new CollectionStep('Update settings', [
                    new ExecuteQueryStep($this->systemDb, "REPLACE INTO isys_settings SET
                        isys_settings__key = 'admin.module." . $this->id . ".installed',
                        isys_settings__value = '" . time() . "',
                        isys_settings__isys_mandator__id = '" . $this->tenantId . "';"),
                    new ExecuteQueryStep($this->systemDb, "REPLACE INTO isys_settings SET
                        isys_settings__key = 'cmdb.renew-properties',
                        isys_settings__value = 1,
                        isys_settings__isys_mandator__id = '" . $this->tenantId . "';")
                ]);

                if ($step->process($messages)) {
                    $this->toUndo[] = $step;
                } else {
                    return false;
                }
            }

            return true;
        } catch (\Exception $e) {
            $messages->addMessage(new StepMessage($this, $e->getMessage(), ErrorLevel::ERROR));
        }

        return false;
    }

    /**
     * Undo the work
     *
     * @param Messages $messages
     *
     * @return mixed
     */
    public function undo(Messages $messages)
    {
        $result = true;

        if ($this->installed) {
            $errors = [];
            $result = $this->moduleManager->uninstallAddOn($this->id, [], $errors);

            foreach ($errors as $error) {
                $messages->addMessage(new StepMessage($this, $error, ErrorLevel::NOTIFICATION));
            }
        }

        foreach ($this->toUndo as $item) {
            $result &= $item->undo($messages);
        }

        return $result;
    }
}
