<?php

namespace Xentral\Modules\Wizard;

use erpAPI;
use Xentral\Components\Database\Database;
use Xentral\Modules\User\Service\UserConfigService;
use Xentral\Modules\Wizard\Exception\InvalidArgumentException;
use Xentral\Modules\Wizard\Exception\NotFoundException;
use Xentral\Modules\Wizard\Exception\WizardExceptionInterface;

final class WizardService
{
    /** @var Database $db */
    private $db;

    /** @var UserConfigService $userConfig */
    private $userConfig;

    /**
     * @deprecated
     * @var erpAPI $erpApi
     */
    private $erpApi;

    /**
     * @param Database          $db
     * @param UserConfigService $userConfig
     * @param erpAPI            $erpApi
     */
    public function __construct(Database $db, UserConfigService $userConfig, erpAPI $erpApi)
    {
        $this->db = $db;
        $this->userConfig = $userConfig;
        $this->erpApi = $erpApi;
    }

    /**
     * @param int $userId
     *
     * @return array
     */
    public function getActiveWizardKeys($userId)
    {
        $userId = $this->ensureUserId($userId);

        $wizards = $this->db->fetchCol(
            'SELECT w.key 
             FROM wizard AS w 
             WHERE w.user_id = :user_id AND w.active = 1 
             ORDER BY w.created_at ASC',
            ['user_id' => $userId]
        );

        if (!is_array($wizards)) {
            return [];
        }

        return $wizards;
    }

    /**
     * @param int $userId
     *
     * @return string|null
     */
    public function getActiveWizardKey($userId)
    {
        $userId = $this->ensureUserId($userId);

        return $this->userConfig->tryGet('active_wizard', $userId);
    }

    /**
     * @param string $wizardKey
     * @param int    $userId
     *
     * @return void
     */
    public function setActiveWizardKey($wizardKey, $userId)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        $this->userConfig->set('active_wizard', $wizardKey, $userId);
    }


    /**
     * @param string $wizardKey
     * @param string $stepKey
     * @param int    $userId
     *
     * @return void
     */
    public function setStepVisited($wizardKey, $stepKey, $userId)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $stepKey = $this->ensureStepKey($stepKey);
        $userId = $this->ensureUserId($userId);

        if ($serializedCompletedWizardSteps = $this->userConfig->tryGet('completed_wizard_steps', $userId)) {
            $completedWizardSteps = unserialize($serializedCompletedWizardSteps, ['allowed_classes' => false]);
        } else {
            $completedWizardSteps = [];
        }

        $completedWizardSteps[$wizardKey][] = $stepKey;

        $this->userConfig->set('completed_wizard_steps', serialize($completedWizardSteps), $userId);

        // if the count of the completed steps are the same as the completed steps, delete the active_wizard key
        $wizard = $this->getWizardFromFile($wizardKey);
        $completableSteps = [];
        foreach ($wizard['step_groups'] as $group) {
            foreach ($group['sub_groups'] as $subGroup) {
                $completableSteps[] = $subGroup;
            }
        }
        if (count($completableSteps) === count($completedWizardSteps[$wizardKey])) {
            $this->userConfig->delete('active_wizard', $userId);
        }
    }

    /**
     * @param string $wizardKey
     * @param string $stepKey
     * @param int    $userId
     *
     * @return bool
     */
    public function hasStepVisited($wizardKey, $stepKey, $userId): bool
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $stepKey = $this->ensureStepKey($stepKey);
        $userId = $this->ensureUserId($userId);

        $value = $this->userConfig->tryGet('wizard_' . $wizardKey . '_step_' . $stepKey, $userId);

        return (int)$value === 1;
    }

    /**
     * @param string $wizardKey
     * @param int    $userId
     *
     * @return string|null
     */
    public function getLastVisitedStep($wizardKey, $userId)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        return $this->userConfig->tryGet('wizard_' . $wizardKey . '_last_visited_step', $userId);
    }

    /**
     * @param string $wizardKey
     * @param int    $userId
     *
     * @return array
     */
    public function getWizard($wizardKey, $userId): array
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        $wizard = $this->getWizardFromFile($wizardKey);

        return $this->prepareWizardData($wizard, $userId);
    }

    protected function getWizardFromFile(string $key)
    {
        $filePath = __DIR__ . '/www/wizards/' . $key . '.json';

        if (!file_exists($filePath)) {
            throw new NotFoundException(sprintf("Wizard with key %s not found", $key));
        }
        $wizardJson = file_get_contents($filePath);

        return json_decode($wizardJson, true);
    }

    /**
     * @param string $wizardKey
     *
     * @return string
     */
    public function getFirstWizardLink(string $wizardKey)
    {
        $wizard = $this->getWizardFromFile($wizardKey);
        $firstStepGroup = current($wizard['step_groups']);
        $firstSubGroup = current($firstStepGroup['sub_groups']);
        $firstStep = current($firstSubGroup['steps']);

        return $firstStep['link'];
    }

    public function saveLastVisitedLink(string $key, string $link, $userId)
    {
        $userId = $this->ensureUserId($userId);

        $lastVisitedWizardLinks = $this->userConfig->tryGet('last_visited_wizard_links', $userId);
        $lastVisitedWizardLinks = $lastVisitedWizardLinks !== null ?
            unserialize($lastVisitedWizardLinks, ['allowed_classes' => false])
            : [];

        $lastVisitedWizardLinks[$key] = $link;

        $this->userConfig->set('last_visited_wizard_links', serialize($lastVisitedWizardLinks), $userId);
    }

    public function isMinimizedForUser($userId)
    {
        $userId = $this->ensureUserId($userId);

        $isMinimized = $this->userConfig->tryGet('wizard_is_minimized', $userId);

        return $isMinimized === null || $isMinimized === false
            ? false
            : true;
    }

    public function setMinimizedForUser($userId, $isMinimized)
    {
        $userId = $this->ensureUserId($userId);

        $this->userConfig->set('wizard_is_minimized', $isMinimized, $userId);

        return $isMinimized;
    }

    public function cancelActiveWizardForUser($userId)
    {
        $userId = $this->ensureUserId($userId);

        // delete wizard_is_minimized key
        $this->userConfig->delete('wizard_is_minimized', $userId);

        // delete active_wizard key
        $this->userConfig->delete('active_wizard', $userId);
    }

    public function isWizardCompletedForUser($wizardKey, $userId): bool
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        $wizard = $this->getWizard($wizardKey, $userId);
        $stepCount = 0;
        $completedStepCount = 0;
        foreach ($wizard['step_groups'] as $stepGroupName => $stepGroupContent) {
            $stepCount++;

            if (isset($stepGroupContent['completed']) && $stepGroupContent['completed'] === true) {
                $completedStepCount++;
            }
        }

        return $stepCount === $completedStepCount;
    }

    public function finishWizardForUser($wizardKey, $userId)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        $this->cancelActiveWizardForUser($userId);

        // Remove last_visited_wizard_links for this wizard key
        $lastVisitedWizardLinks = $this->userConfig->tryGet('last_visited_wizard_links', $userId);
        $lastVisitedWizardLinks = $lastVisitedWizardLinks !== null
            ? unserialize($lastVisitedWizardLinks, ['allowed_classes' => false])
            : [];
        unset($lastVisitedWizardLinks[$wizardKey]);
        $this->userConfig->set('last_visited_wizard_links', serialize($lastVisitedWizardLinks), $userId);
    }

    public function resetWizardForUser($wizardKey, $userId)
    {
        $this->finishWizardForUser($wizardKey, $userId);
        // Remove completed_wizard_steps for this wizard key
        $completedWizardSteps = $this->userConfig->tryGet('completed_wizard_steps', $userId);
        $completedWizardSteps = $completedWizardSteps !== null
            ? unserialize($completedWizardSteps, ['allowed_classes' => false])
            : [];
        unset($completedWizardSteps[$wizardKey]);
        $this->userConfig->set('completed_wizard_steps', serialize($completedWizardSteps), $userId);
    }

    /**
     * @param string $wizardKey
     * @param int    $userId
     *
     * @throws NotFoundException
     *
     * @return void
     */
    public function activateWizard($wizardKey, $userId)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        $wizardId = $this->getWizardIdByKey($wizardKey, $userId);
        if ($wizardId === false) {
            throw new NotFoundException(
                sprintf(
                    'Wizard with key "%s" and user id "%s" not found.',
                    $wizardKey,
                    $userId
                )
            );
        }

        $this->db->perform(
            'UPDATE wizard SET active = 1 WHERE id = :wizard_id AND user_id = :user_id',
            ['wizard_id' => $wizardId, 'user_id' => $userId]
        );
    }

    /**
     * @param string $wizardKey
     * @param int    $userId
     *
     * @throws NotFoundException
     *
     * @return void
     */
    public function deactivateWizard($wizardKey, $userId)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        $wizardId = $this->getWizardIdByKey($wizardKey, $userId);
        if ($wizardId === false) {
            throw new NotFoundException(
                sprintf(
                    'Wizard with key "%s" and user id "%s" not found.',
                    $wizardKey,
                    $userId
                )
            );
        }

        $this->db->perform(
            'UPDATE wizard SET active = 0 WHERE id = :wizard_id AND user_id = :user_id',
            ['wizard_id' => $wizardId, 'user_id' => $userId]
        );
    }

    /**
     * @param string $wizardKey
     * @param int    $userId
     *
     * @throws NotFoundException
     *
     * @return void
     */
    public function deleteWizard($wizardKey, $userId)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        $wizardId = $this->getWizardIdByKey($wizardKey, $userId);
        if ($wizardId === false) {
            throw new NotFoundException(
                sprintf(
                    'Wizard with key "%s" and user id "%s" not found.',
                    $wizardKey,
                    $userId
                )
            );
        }

        $this->deleteWizardById($wizardId);
    }

    /**
     * @param int $wizardId
     *
     * @return void
     */
    public function deleteWizardById($wizardId)
    {
        $wizardId = $this->ensureWizardId($wizardId);

        $this->db->beginTransaction();
        $this->db->perform('DELETE FROM wizard_step WHERE wizard_id = :wizard_id', ['wizard_id' => $wizardId]);
        $this->db->perform('DELETE FROM wizard WHERE id = :wizard_id LIMIT 1', ['wizard_id' => $wizardId]);
        $this->db->commit();
    }

    /**
     * @param int $wizardId
     *
     * @return array
     */
    public function generateTemplateFromExistingWizard($wizardId)
    {
        $wizardId = $this->ensureWizardId($wizardId);

        $settings = $this->db->fetchRow(
            'SELECT w.user_id, w.active, w.key, w.title, w.skip_link_text, w.params, w.options 
             FROM wizard AS w 
             WHERE w.id = :wizard_id',
            ['wizard_id' => $wizardId]
        );
        $settings['params'] = json_decode($settings['params'], true);
        $settings['options'] = json_decode($settings['options'], true);
        $settings['active'] = (bool)$settings['active'];

        $steps = $this->db->fetchAll(
            'SELECT s.key, s.link, s.title, s.caption, s.description, s.position, s.options 
             FROM wizard_step AS s 
             WHERE s.wizard_id = :wizard_id 
             ORDER BY s.position ASC, s.created_at ASC, s.id ASC',
            ['wizard_id' => $wizardId]
        );
        $position = 1;
        foreach ($steps as &$step) {
            $step['options'] = json_decode($step['options'], true);
            $step['position'] = $position;
            $position++;
        }

        return [
            'settings' => $settings,
            'steps'    => $steps,
        ];
    }

    /**
     * Creates or updates an wizard from decoded JSON template
     *
     * @param array    $data
     * @param int|null $overwriteUserId If not null, wizard will be assigned to this user
     *
     * @return int Updated or created wizard id
     */
    public function replaceWizard($data, $overwriteUserId = null)
    {
        if (!is_array($data)) {
            throw new InvalidArgumentException('Data is not an array.');
        }
        if (empty($data['settings'])) {
            throw new InvalidArgumentException('Settings property is empty.');
        }
        if (empty($data['steps'])) {
            throw new InvalidArgumentException('Steps property is empty.');
        }

        try {
            $stepIds = [];
            $this->db->beginTransaction();
            $wizardId = $this->replaceSettings($data['settings'], $overwriteUserId);
            foreach ($data['steps'] as $step) {
                $stepIds[] = $this->replaceSteps($step, $wizardId);
            }
            // Delete removed steps
            $this->db->perform(
                'DELETE FROM wizard_step WHERE wizard_id = :wizard_id AND id NOT IN (:step_ids)',
                ['wizard_id' => $wizardId, 'step_ids' => $stepIds]
            );
            $this->db->commit();

            return $wizardId;
            //
        } catch (WizardExceptionInterface $e) {
            $this->db->rollBack();
            throw $e;
        }
    }

    /**
     * @param string $wizardKey
     * @param string $stepKey
     * @param int    $userId
     * @param bool   $checked
     *
     * @throws NotFoundException
     *
     * @return void
     */
    public function setStepChecked($wizardKey, $stepKey, $userId, $checked = true)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $stepKey = $this->ensureStepKey($stepKey);
        $userId = $this->ensureUserId($userId);
        $checked = (bool)$checked;

        $stepId = (int)$this->db->fetchValue(
            'SELECT s.id 
             FROM wizard_step AS s 
             INNER JOIN wizard AS w ON w.id = s.wizard_id 
             WHERE w.key = :wizard_key AND s.key = :step_key AND w.user_id = :user_id',
            ['wizard_key' => $wizardKey, 'step_key' => $stepKey, 'user_id' => $userId]
        );

        if ($stepId === 0) {
            throw new NotFoundException(
                sprintf(
                    'Step not found. wizard_key="%s", step_key="%s", user_id="%s"',
                    $wizardKey,
                    $stepKey,
                    $userId
                )
            );
        }

        $this->db->perform(
            'UPDATE wizard_step SET checked = :checked WHERE id = :step_id LIMIT 1',
            ['step_id' => $stepId, 'checked' => $checked === true ? 1 : 0]
        );
    }

    /**
     * @param string $wizardKey
     * @param int    $userId
     *
     * @throws NotFoundException
     *
     * @return array
     */
    private function loadSettings($wizardKey, $userId)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        $settings = $this->db->fetchRow(
            'SELECT w.id, w.user_id, w.key, w.title, w.skip_link_text, w.params, w.options, w.active 
             FROM `wizard` AS `w` 
             WHERE w.key = :wizard_key AND w.user_id = :user_id',
            ['wizard_key' => $wizardKey, 'user_id' => $userId]
        );

        if ($settings === false) {
            throw new NotFoundException(
                sprintf(
                    'Wizard with key "%s" and user id "%s" not found.',
                    $wizardKey,
                    $userId
                )
            );
        }

        $settings['id'] = (int)$settings['id'];
        $settings['user_id'] = (int)$settings['user_id'];
        $settings['active'] = (int)$settings['active'] === 1;
        $settings['params'] = json_decode($settings['params'], true);
        $settings['options'] = json_decode($settings['options'], true);
        $settings['skip_link_text'] =
            !empty($settings['skip_link_text'])
                ? $settings['skip_link_text']
                : 'Wizard überspringen';

        return $settings;
    }

    /**
     * @param string $wizardKey
     * @param int    $userId
     *
     * @throws NotFoundException
     *
     * @return array
     */
    private function loadSteps($wizardKey, $userId)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        $steps = $this->db->fetchAll(
            'SELECT 
               s.id, s.wizard_id, s.key, s.link, s.title, s.caption, 
               s.description, s.position, s.options, s.checked 
             FROM wizard_step AS s 
             INNER JOIN wizard AS w ON w.id = s.wizard_id
             WHERE w.key = :wizard_key AND w.user_id = :user_id 
             ORDER BY s.position ASC, s.created_at ASC, s.id ASC',
            ['wizard_key' => $wizardKey, 'user_id' => $userId]
        );

        if ($steps === false) {
            throw new NotFoundException(
                sprintf(
                    'Wizard steps with key "%s" and user id "%s" not found.',
                    $wizardKey,
                    $userId
                )
            );
        }

        foreach ($steps as &$step) {
            $step['id'] = (int)$step['id'];
            $step['wizard_id'] = (int)$step['wizard_id'];
            $step['checked'] = (int)$step['checked'] === 1;
            $step['options'] = json_decode($step['options'], true);
            $step['position'] = (int)$step['position'];
        }

        return $steps;
    }

    /**
     * @param array $wizard
     * @param int   $userId
     *
     * @return array
     */
    private function prepareWizardData(array $wizard, int $userId)
    {
        $wizard = $this->addCompletedSteps($wizard, $userId);
        $wizard = $this->addLastVisitedLink($wizard, $userId);
        $wizard = $this->addMissingPermissions($wizard);
        if ($this->isMinimizedForUser($userId)) {
            $wizard['minimized'] = true;
        }

        return $wizard;
    }

    private function addCompletedSteps(array $wizard, int $userId)
    {
        if ($serializedCompletedWizardSteps = $this->userConfig->tryGet('completed_wizard_steps', $userId)) {
            $completedWizardSteps = unserialize($serializedCompletedWizardSteps, ['allowed_classes' => false]);
            if (isset($completedWizardSteps[$wizard['key']])) {
                $completedWizardSteps = array_map(
                    function ($step) {
                        return explode('-', $step);
                    },
                    $completedWizardSteps[$wizard['key']]
                );
                foreach ($completedWizardSteps as [$stepGroup, $subGroup]) {
                    $wizard['step_groups'][$stepGroup]['sub_groups'][$subGroup]['completed'] = true;
                }

                // add complete to the step_group if all sub_groups are completed
                foreach ($wizard['step_groups'] as $key => $stepGroup) {
                    $subGroupCount = count($stepGroup['sub_groups']);
                    $completedSubGroups = array_filter(
                        $stepGroup['sub_groups'],
                        function ($subGroup) {
                            return $subGroup['completed'] === true;
                        }
                    );

                    if ($subGroupCount === count($completedSubGroups)) {
                        $wizard['step_groups'][$key]['completed'] = true;
                    }
                }
            }
        }

        return $wizard;
    }

    private function addLastVisitedLink(array $wizard, int $userId)
    {
        if (!$lastVisitedWizardLinks = $this->userConfig->tryGet('last_visited_wizard_links', $userId)) {
            return $wizard;
        }
        $lastVisitedWizardLinks = unserialize($lastVisitedWizardLinks, ['allowed_classes' => false]);

        if (!isset($lastVisitedWizardLinks[$wizard['key']])) {
            return $wizard;
        }
        $lastVisitedLink = $lastVisitedWizardLinks[$wizard['key']];

        foreach ($wizard['step_groups'] as $setGroupKey => $stepGroup) {
            foreach ($stepGroup['sub_groups'] as $subGroupKey => $subGroup) {
                if (!isset($subGroup['completed']) || $subGroup['completed'] === false) {
                    $wizard['step_groups'][$setGroupKey]['sub_groups'][$subGroupKey]['link'] = $lastVisitedLink;
                }
            }
        }

        return $wizard;
    }

    /**
     * @param array    $data
     * @param int|null $overwriteUserId If not null, wizard will be assigned to this user
     *
     * @return int Updated or created wizard id
     */
    private function replaceSettings($data, $overwriteUserId = null)
    {
        $wizardKey = (string)$data['key'];
        $userId = $overwriteUserId;
        if ($userId === null || (int)$userId === 0) {
            $userId = $data['user_id'];
        }

        // Check required parameter
        if (empty($wizardKey)) {
            throw new InvalidArgumentException('Required settings property "key" is missing or empty.');
        }
        if (empty($data['title'])) {
            throw new InvalidArgumentException('Required settings property "title" is missing or empty.');
        }
        if (empty($userId) || (int)$userId === 0) {
            throw new InvalidArgumentException('Required settings property "user_id" is missing or empty.');
        }

        $wizardId = $this->getWizardIdByKey($wizardKey, $userId);

        // Create new wizard
        if ($wizardId === false) {
            $insert = $this->db->insert();
            $insert
                ->cols(
                    [
                        'user_id'        => $userId,
                        'key'            => $wizardKey,
                        'active'         => (int)$data['active'],
                        'title'          => (string)$data['title'],
                        'skip_link_text' => $data['skip_link_text'],
                        'params'         => json_encode($data['params']),
                        'options'        => json_encode($data['options']),
                    ]
                )
                ->set('created_at', 'NOW()')
                ->into('wizard');
            $this->db->perform($insert->getStatement(), $insert->getBindValues());

            return $this->db->lastInsertId();
        }

        // Update existing wizard
        $update = $this->db->update();
        $update
            ->table('wizard AS w')
            ->cols(
                [
                    'key',
                    'title',
                    'skip_link_text',
                    'active',
                    'params',
                    'options',
                ]
            )
            ->where('w.id = :wizard_id')
            ->where('w.user_id = :user_id')
            ->bindValues(
                [
                    'user_id'        => $userId,
                    'wizard_id'      => $wizardId,
                    'key'            => $wizardKey,
                    'active'         => (int)$data['active'],
                    'title'          => (string)$data['title'],
                    'skip_link_text' => $data['skip_link_text'],
                    'params'         => json_encode($data['params']),
                    'options'        => json_encode($data['options']),
                ]
            )
            ->limit(1);

        $this->db->perform($update->getStatement(), $update->getBindValues());

        return $wizardId;
    }

    /**
     * @param array $data
     * @param int   $wizardId
     *
     * @return int Updated or created step id
     */
    private function replaceSteps($data, $wizardId)
    {
        // Check required parameter
        if (empty($wizardId) || (int)$wizardId === 0) {
            throw new InvalidArgumentException('Required property "wizard_id" is missing or empty.');
        }
        if (empty($data['key'])) {
            throw new InvalidArgumentException('Required property "key" is missing or empty.');
        }
        if (empty($data['link'])) {
            throw new InvalidArgumentException('Required property "link" is missing or empty.');
        }
        if (empty($data['title'])) {
            throw new InvalidArgumentException('Required property "title" is missing or empty.');
        }

        $stepId = $this->getStepIdByKey($data['key'], $wizardId);

        // Create new step
        if ($stepId === false) {
            $insert = $this->db->insert();
            $insert
                ->cols(
                    [
                        'wizard_id'   => $wizardId,
                        'key'         => $data['key'],
                        'link'        => $data['link'],
                        'title'       => $data['title'],
                        'caption'     => $data['caption'],
                        'description' => substr($data['description'], 0, 1024),
                        'position'    => (int)$data['position'],
                        'options'     => json_encode($data['options']),
                        'checked'     => 0,
                    ]
                )
                ->set('created_at', 'NOW()')// Raw value
                ->into('wizard_step');
            $this->db->perform($insert->getStatement(), $insert->getBindValues());

            return $this->db->lastInsertId();
        }

        // Update existing step
        $update = $this->db->update();
        $update
            ->table('wizard_step AS s')
            ->cols(
                [
                    'link',
                    'title',
                    'caption',
                    'description',
                    'position',
                    'options',
                    'checked',
                ]
            )
            ->where('s.id = :step_id')
            ->where('s.wizard_id = :wizard_id')
            ->bindValues(
                [
                    'step_id'     => $stepId,
                    'wizard_id'   => $wizardId,
                    'key'         => $data['key'],
                    'link'        => $data['link'],
                    'title'       => $data['title'],
                    'caption'     => $data['caption'],
                    'description' => substr($data['description'], 0, 1024),
                    'position'    => (int)$data['position'],
                    'options'     => json_encode($data['options']),
                    'checked'     => 0,
                ]
            )
            ->limit(1);

        $this->db->perform($update->getStatement(), $update->getBindValues());

        return $stepId;
    }

    /**
     * @param array $options
     * @param array $params
     *
     * @return array Prepared options
     */
    private function prepareStepOptions($options = [], $params = [])
    {
        if (!is_array($options)) {
            return [];
        }
        if (!is_array($params)) {
            return $options;
        }

        foreach ($options as &$value) {
            if (is_array($value)) {
                $value = $this->prepareStepOptions($value, $params);
            }

            // Replace parameter values
            // Example: "foo_##shop_id##_bar" will be replace with "foo_1_bar", if $param['shop_id'] = 1
            preg_match_all('/\#\#(.+?)\#\#/', $value, $matches);
            if (!empty($matches[1])) {
                foreach ($matches[1] as $index => $paramName) {
                    if (isset($params[$paramName])) {
                        $replaceCount = 1;
                        $search = $matches[0][$index];
                        $replace = $params[$paramName];
                        $value = str_replace($search, $replace, $value, $replaceCount);
                    }
                }
            }
        }

        return $options;
    }

    /**
     * @param string $moduleName
     * @param string $methodName
     * @param array  $arguments
     *
     * @return bool
     */
    private function checkModuleCallback($moduleName, $methodName, $arguments = [])
    {
        $module = $this->erpApi->LoadModul($moduleName);
        if (!empty($module) && method_exists($module, $methodName)) {
            return (bool)call_user_func_array([$module, $methodName], $arguments);
        }

        return false;
    }

    /**
     * @param string $objectName
     * @param string $actionName
     * @param int    $objectId
     * @param int    $userId
     *
     * @return bool
     */
    private function checkObjectProtocolEntry($objectName, $actionName, $objectId, $userId)
    {
        $this->ensureUserId($userId);
        $username = $this->getUserNameById($userId);

        $count = $this->db->fetchValue(
            'SELECT COUNT(o.id) AS num 
             FROM objekt_protokoll AS o 
             WHERE o.objekt = :object_name AND o.objektid = :object_id 
             AND o.action_long = :action_name AND o.bearbeiter = :username',
            [
                'object_name' => (string)$objectName,
                'action_name' => (string)$actionName,
                'object_id'   => (int)$objectId,
                'username'    => (string)$username,
            ]
        );

        return (int)$count > 0;
    }

    /**
     * @param string $wizardKey
     * @param int    $userId
     *
     * @return int|false
     */
    private function getWizardIdByKey($wizardKey, $userId)
    {
        $wizardKey = $this->ensureWizardKey($wizardKey);
        $userId = $this->ensureUserId($userId);

        $wizardId = $this->db->fetchValue(
            'SELECT w.id FROM wizard AS w WHERE w.key = :key AND w.user_id = :user_id LIMIT 1',
            ['key' => $wizardKey, 'user_id' => $userId]
        );

        if ($wizardId === false) {
            return false;
        }

        return (int)$wizardId;
    }

    /**
     * @param string $stepKey
     * @param int    $wizardId
     *
     * @return int|false
     */
    private function getStepIdByKey($stepKey, $wizardId)
    {
        $stepKey = $this->ensureStepKey($stepKey);
        $wizardId = $this->ensureWizardId($wizardId);

        $stepId = $this->db->fetchValue(
            'SELECT s.id 
             FROM wizard_step AS s
             INNER JOIN wizard AS w ON s.wizard_id = w.id
             WHERE s.key = :step_key AND s.wizard_id = :wizard_id 
             LIMIT 1',
            ['step_key' => $stepKey, 'wizard_id' => $wizardId]
        );

        if ($stepId === false) {
            return false;
        }

        return (int)$stepId;
    }

    /**
     * @param int $userId
     *
     * @return string|false
     */
    private function getUserNameById($userId)
    {
        $userId = $this->ensureUserId($userId);

        $username = $this->db->fetchValue(
            'SELECT a.name FROM `user` AS u 
             INNER JOIN adresse AS a ON u.adresse = u.id
             WHERE u.id = :user_id',
            ['user_id' => $userId]
        );

        if ($username === false) {
            return false;
        }

        return (string)$username;
    }

    /**
     * @param string $key
     *
     * @throws InvalidArgumentException
     *
     * @return string
     */
    private function ensureWizardKey($key)
    {
        if (empty($key)) {
            throw new InvalidArgumentException('Required parameter "wizardKey" is empty.');
        }

        $filePath = __DIR__ . '/www/wizards/' . $key . '.json';
        if (!file_exists($filePath)) {
            throw new NotFoundException(sprintf("Wizard with key %s not found", $key));
        }

        return (string)$key;
    }

    /**
     * @param string $key
     *
     * @throws InvalidArgumentException
     *
     * @return string
     */
    private function ensureStepKey($key)
    {
        if (empty($key)) {
            throw new InvalidArgumentException('Required parameter "stepKey" is empty.');
        }

        return (string)$key;
    }

    /**
     * @param int $userId
     *
     * @throws InvalidArgumentException
     *
     * @return int
     */
    private function ensureUserId($userId)
    {
        if ((int)$userId === 0) {
            throw new InvalidArgumentException('Required parameter "userId" is invalid.');
        }

        return (int)$userId;
    }


    /**
     * @param int $wizardId
     *
     * @throws InvalidArgumentException
     *
     * @return int
     */
    private function ensureWizardId($wizardId)
    {
        if ((int)$wizardId === 0) {
            throw new InvalidArgumentException('Required parameter "wizardId" is invalid.');
        }

        return (int)$wizardId;
    }

    /**
     * @param array $wizard
     * @param int   $userId
     *
     * @return array
     */
    private function addMissingPermissions(array $wizard)
    {
        if (!isset($wizard['required_permissions'])) {
            return $wizard;
        }

        $missingPermissions = [];
        $missingModules = [];
        foreach ($wizard['required_permissions'] as $module => $actions) {
            $missing = [];
            if (!$this->erpApi->ModulVorhanden($module)) {
                $missingModules[] = $module;
                continue;
            }
            foreach ($actions as $action) {
                if (!$this->erpApi->RechteVorhanden($module, $action)) {
                    $missing[] = $action;
                }
            }
            if (!empty($missing)) {
                $missingPermissions[$module] = $missing;
            }
        }

        unset($wizard['required_permissions']);
        $wizard['missing_modules'] = $missingModules;
        $wizard['missing_permissions'] = $missingPermissions;

        return $wizard;
    }
}