<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

namespace Shopware\Plugins\ViisonShippingCommon\Controllers\Backend;

use Doctrine\ORM\Query;
use Shopware\CustomModels\ViisonShippingCommon\Configuration\Configuration;
use Shopware\Models\Shop\Shop;
use Shopware\Plugins\ViisonCommon\Controllers\ViisonCommonBaseController;
use Shopware\Plugins\ViisonShippingCommon\Classes\ConfigurationPropertyEncryption;
use Shopware\Plugins\ViisonShippingCommon\Classes\Exceptions\CryptographyException;
use Shopware\Plugins\ViisonShippingCommon\Util;

/**
 * This controller adds some actions called in the config backend controller.
 * These actions are responsible for creating, updating and deleting custom configurations
 * of the dispatch service provider plugin which are associated with (sub-)shop configurations.
 */
class ViisonShippingCommonConfig extends ViisonCommonBaseController
{
    const DUMMY_SECRET = '************';

    protected $pluginInfo;
    protected $util;
    protected $configTable;

    public function __construct(\Enlight_Controller_Request_Request $request = null, \Enlight_Controller_Response_Response $response = null, Util $util = null)
    {
        parent::__construct($request, $response);
        if ($util != null) {
            $this->pluginInfo = $util->getPluginInfo();
            $this->util = $util;
            $this->configTable = new \Zend_Db_Table($this->pluginInfo->getConfigTableName());
        }
    }

    /**
     * @inheritdoc
     */
    public function getViewParams()
    {
        return array(
            'shippingConfigFields' => ($this->util) ? $this->util->getConfigFieldDescriptions() : array()
        );
    }

    /* Actions */

    /**
     * Gathers all shop dispatch service provider configurations. This method also updates the encryption of encrypted
     * fields to the one given by Util::getOptimalEncryptionMethod(). Doing so simplifies the createOrUpdateConfiguration
     * method, because sure by doing we can be so that no outdated encrypted values are passed if the user does not change
     * the encrypted value in the configuration dialog.
     */
    public function getAllConfigurationsAction()
    {
        $optimalEncryptionMethod = $this->util->getOptimalEncryptionMethod();
        if ($this->pluginInfo->getConfigurationModelName() === null) {
            /**
             * @deprecated block
             */
            // Get all dispatch service provider shop configurations
            $result = $this->configTable->fetchAll($this->configTable->select())->toArray();


            foreach ($result as $index => &$row) {
                $outdatedEncryptionMethod = array_key_exists('encryptionMethod', $row);
                $outdatedEncryptionMethod &= $row['encryptionMethod'] != $optimalEncryptionMethod;
                foreach ($this->getEncryptedFields() as $field) {
                    if (empty($row[$field])) {
                        continue;
                    }

                    // Change the encryption method if an outdated method was used before
                    if ($outdatedEncryptionMethod) {
                        try {
                            $row[$field] = $this->util->encrypt(
                                $this->util->decrypt($row[$field], $row['encryptionMethod']),
                                $optimalEncryptionMethod
                            );
                        } catch (\Exception $e) {
                            $row[$field] = '';
                        }
                        // Save the change to the database
                        $this->configTable->update(
                            array(
                                $field => $row[$field]
                            ),
                            $this->configTable->getAdapter()->quoteInto('id = ?', $row['id'])
                        );
                    }
                    // Set encrypted fields to an empty string if their decrypted value is empty
                    try {
                        if ($this->util->decrypt($row[$field], $this->util->getDecryptionMethod($row)) == '') {
                            $row[$field] = '';
                        }
                    } catch (\Exception $e) {
                        $row[$field] = '';
                    }
                }
                // Save the new encryption method to the database
                if ($outdatedEncryptionMethod) {
                    $this->configTable->update(
                        array(
                            'encryptionMethod' => $optimalEncryptionMethod
                        ),
                        $this->configTable->getAdapter()->quoteInto('id = ?', $row['id'])
                    );
                }

            }
        } else {
            $entityManager = $this->get('models');
            $modelName = $this->pluginInfo->getConfigurationModelName();
            $allConfigurations = $entityManager->getRepository($modelName)->findAll();

            foreach ($allConfigurations as $configuration) {
                $propertyEncryption = new ConfigurationPropertyEncryption($configuration, $this->util->getEncryptionKey());
                // Upgrade the config's encryption method, if possible
                if ($configuration->getEncryptionMethod() !== $optimalEncryptionMethod) {
                    $decryptedValues = array();
                    foreach ($configuration->getEncryptedProperties() as $propertyName) {
                        try {
                            $decryptedValues[$propertyName] = $propertyEncryption->getDecryptedValue($propertyName);
                        } catch (CryptographyException $e) {
                            // Clear the password when the decryption fails for any reason.
                            $decryptedValues[$propertyName] = '';
                        }
                    }
                    $configuration->setEncryptionMethod($optimalEncryptionMethod);
                    foreach ($decryptedValues as $propertyName => $value) {
                        $propertyEncryption->encryptAndSetValue($propertyName, $value);
                    }
                }

                // Set encrypted fields to an empty string, if their decrypted value is empty
                foreach ($configuration->getEncryptedProperties() as $propertyName) {
                    try {
                        $decryptedValue = $propertyEncryption->getDecryptedValue($propertyName);
                    } catch (CryptographyException $e) {
                        // Clear the password when the decryption fails for any reason.
                        $decryptedValue = '';
                    }
                    if ($decryptedValue === '') {
                        $setterName = 'set' . ucfirst($propertyName);
                        $configuration->{$setterName}('');
                    }
                }
            }

            // Save changes
            $entityManager->flush($allConfigurations);

            // Fetch all configurations again as arrays
            $builder = $entityManager->createQueryBuilder();
            $builder
                ->select('configuration')
                ->from($modelName, 'configuration');
            $result = $builder->getQuery()->getArrayResult();
        }


        foreach ($this->getSecretFields() as $field) {
            foreach ($result as &$config) {
                if (!empty($config[$field])) {
                    $config[$field] = self::DUMMY_SECRET;
                }
            }
        }

        $this->View()->assign(array(
            'success' => true,
            'data' => $result
        ));
    }

    /**
     * Creates a new dispatch service provider configuration for a shop.
     */
    public function createConfigurationAction()
    {
        // Get all request parameters
        $configData = $this->Request()->getParams();
        if (empty($configData['shopId'])) {
            $this->View()->assign(array(
                'success' => false,
                'message' => 'Missing shop id.',
                'params' => $this->Request()->getParams()
            ));

            return;
        }

        // Update the configuration
        $result = $this->createOrUpdateConfiguration($configData);

        $this->View()->assign(array(
            'success' => true,
            'data' => $result,
            'params' => $this->Request()->getParams()
        ));
    }

    /**
     * Updates a dispatch service provider configuration with the given data.
     */
    public function updateConfigurationAction()
    {
        // Get all request parameters
        $configData = $this->Request()->getParams();
        if (empty($configData['shopId'])) {
            $this->View()->assign(array(
                'success' => false,
                'message' => 'Missing shop id.'
            ));

            return;
        }

        // Update the configuration
        $result = $this->createOrUpdateConfiguration($configData);

        $this->View()->assign(array(
            'success' => true,
            'data' => $result
        ));
    }

    /**
     * Deletes the dispatch service provider configuration of a shop.
     */
    public function destroyConfigurationAction()
    {
        // Get all shop id
        $shopId = $this->Request()->getParam('shopId');
        if (empty($shopId)) {
            $this->View()->assign(array(
                'success' => false,
                'message' => 'Missing shop id.'
            ));

            return;
        }

        // Delete the configuration from the database
        if ($this->pluginInfo->getConfigurationModelName() === null) {
            /**
             * @deprecated block
             */
            $this->configTable->delete($this->configTable->getAdapter()->quoteInto('shopId = ?', $shopId));
        } else {
            $entityManager = $this->get('models');

            $config = $entityManager->getRepository(
                $this->pluginInfo->getConfigurationModelName()
            )->findOneByShopId($shopId);

            $entityManager->remove($config);
            $entityManager->flush($config);
        }

        $this->View()->assign(array(
            'success' => true,
            'data' => array()
        ));
    }

    /* Private methods */

    /**
     * Creates or updates a dispatch service provider configuration in the database. That is, if a configuration with
     * the same shop is given already exists, it is updated. Finally the created/updated row is fetched again
     * and returned.
     *
     * @param array $configData An array containing all values to save in the database row incl. the shop id.
     * @return array The created/updated row.
     */
    private function createOrUpdateConfiguration($configData)
    {
        if ($this->pluginInfo->getConfigurationModelName() === null) {
            /**
             * @deprecated block
             */
            // Check if config with given shop id already exists
            $config = $this->configTable->fetchRow(
                $this->configTable->select()
                    ->where('shopId = ?', $configData['shopId'])
            );

            $encryptionMethod = $this->util->getOptimalEncryptionMethod();

            $data = array();
            foreach ($this->getFields() as $field) {
                $data[$field] = $configData[$field];
            }

            foreach ($this->getSecretFields() as $field) {
                if ($data[$field] === self::DUMMY_SECRET) {
                    unset($data[$field]);
                }
            }

            foreach ($this->getEncryptedFields() as $field) {
                // If the field was not changed, it already contains the encrypted value
                if (!empty($config) && $data[$field] == $config[$field]) {
                    continue;
                }
                $data[$field] = $this->util->encrypt($data[$field], $encryptionMethod);
            }

            foreach ($this->getPreProcessingFields() as $field => $func) {
                $data[$field] = $func($data[$field]);
            }

            if ($this->util->hasConfigField('encryptionMethod')) {
                $data['encryptionMethod'] = $encryptionMethod;
            }

            // Add a new or update an existing configuration with the given parameters
            if (empty($config)) {
                $data['shopId'] = $configData['shopId'];
                $this->configTable->insert($data);
            } else {
                $this->configTable->update(
                    $data,
                    $this->configTable->getAdapter()->quoteInto('shopId = ?', $configData['shopId'])
                );
            }

            // Get the created/updated record again as the response data
            $result = $this->configTable->fetchRow(
                $this->configTable->select()
                    ->where('shopId = ?', $configData['shopId'])
            )->toArray();

            foreach ($this->getEncryptedFields() as $field) {
                $result[$field] = $configData[$field];
            }
        } else {
            foreach ($this->getSecretFields() as $field) {
                if ($configData[$field] === self::DUMMY_SECRET) {
                    unset($configData[$field]);
                }
            }

            $modelName = $this->pluginInfo->getConfigurationModelName();
            $entityManager = $this->get('models');
            $originalData = $configData;

            /** @var Configuration $configuration */
            $configuration = $entityManager->getRepository($modelName)->findOneBy(array(
                'shopId' => $configData['shopId']
            ));

            if (!$configuration) {
                // Create new configuration
                $configuration = new $modelName();
                $entityManager->persist($configuration);
                $encryptionMethod = $this->util->getOptimalEncryptionMethod();
                $configuration->setEncryptionMethod($encryptionMethod);
            }

            // Update values of encrypted properties
            $propertyEncryption = new ConfigurationPropertyEncryption($configuration, $this->util->getEncryptionKey());
            foreach ($configuration->getEncryptedProperties() as $propertyName) {
                if (!isset($configData[$propertyName])) {
                    continue;
                }

                $getterName = 'get' . ucfirst($propertyName);
                $encryptedValue = $configuration->{$getterName}();
                if ($configData[$propertyName] !== $encryptedValue) {
                    $propertyEncryption->encryptAndSetValue($propertyName, $configData[$propertyName]);
                }
                unset($configData[$propertyName]);
            }

            if ($configData['shopId'] && !$configuration->getShop()) {
                /** @var Shop $shop */
                $shop = $this->get('models')->find('Shopware\Models\Shop\Shop', $configData['shopId']);
                $configuration->setShop($shop);
            }

            // Update all other values
            $configuration->fromArray($configData);

            // Save changes
            $entityManager->flush($configuration);

            // Fetch all configuration again as an array
            $builder = $entityManager->createQueryBuilder();
            $builder
                ->select('config')
                ->from($modelName, 'config')
                ->where('config.id = :id')
                ->setParameter('id', $configuration->getId());
            $result = $builder->getQuery()->getOneOrNullResult(Query::HYDRATE_ARRAY);

            // Map back encrypted fields to human readable form
            foreach ($configuration->getEncryptedProperties() as $propertyName) {
                $result[$propertyName] = $originalData[$propertyName];
            }
        }

        foreach ($this->getSecretFields() as $field) {
            if (!empty($result[$field])) {
                $result[$field] =  self::DUMMY_SECRET;
            }
        }

        return $result;
    }

    /**
     * Returns an array containing all field keys that are considered secret. These fields will not be send to the client.
     * 
     * @return array
     */
    protected function getSecretFields()
    {
        return [];
    }

    /**
     * @deprecated Use $entityManager->getClassMetadata('CLASS')->getFieldNames();
     *
     * Returns an array containing all field keys that belong to the configuration.
     * @return array
     */
    protected function getFields()
    {
        if (!$this->util) {
            return array();
        }

        // Get the field names from the config field descriptions
        $fields = array_filter(array_map(function ($description) {
            return $description['name'];
        }, $this->util->getConfigFieldDescriptions()), function ($field) {
            return !in_array($field, array('id', 'shopId'));
        });

        return $fields;
    }

    /**
     * @deprecated Use Configuration model instead
     *
     * Returns an array containing all field keys that should be stored encrypted.
     * @return array
     */
    protected function getEncryptedFields()
    {
        return array();
    }

    /**
     * @deprecated Use Configuration model instead
     *
     * Returns an array that assigns functions to configuration field keys. These functions are passed newly
     * entered configuration values before the respective values are stored in the database. Fields whose key
     * is not contained in this array are stored unchanged.
     * @return array
     */
    protected function getPreProcessingFields()
    {
        return array();
    }
}
